Menu

TypeScript 命名空间是什么,什么时候该用

TypeScript 命名空间把值和类型归到一个名称下,编译后是一个普通对象。本文讲解它的语法,命名空间之间以及与函数和类的合并,为什么 ES 模块取代了它,以及你仍会遇到它的地方:声明文件和全局扩充。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

命名空间是一个具名代码块,把函数、常量和类型归到一个名称下。标记了 export 的成员可以通过 Name.member 访问,其余成员则是代码块私有的。运行时,命名空间就是一个普通对象。

命名空间编译成什么

命名空间是少数会生成代码的 TypeScript 特性之一。上面的代码块会变成一个填充对象的函数:

var Geometry;
(function (Geometry) {
    const TAU = Math.PI * 2;
    function circumference(radius) {
        return TAU * radius;
    }
    Geometry.circumference = circumference;
    function area(radius) {
        return Math.PI * radius ** 2;
    }
    Geometry.area = area;
})(Geometry || (Geometry = {}));

这是模块出现之前 JavaScript 代码避免把所有名称都放到全局作用域的做法。TAU 是这个函数的局部变量,所以其他代码看不到它。试图读取它会产生编译错误:

编译器报告 index.ts(10,17): error TS2339: Property 'rate' does not exist on type 'typeof Tax'.。在 const rate 前面加上 export,两行就都能运行了。

嵌套、合并与别名

命名空间可以嵌套,两个同名的代码块会合并成一个。输出中的 Geometry || (Geometry = {}) 正是合并的原理:第二个代码块会往已有的对象上添加成员。import X = A.B 可以创建一个简短的别名。

namespace A.B.C { } 是三个嵌套代码块的简写。旧写法 module Shop { } 含义相同,但现在会被拒绝,报错 TS1540:A 'namespace' declaration should not be declared using the 'module' keyword. Please use the 'namespace' keyword instead.

与函数和类合并

命名空间可以和函数、类或枚举同名,并给它们添加成员。要描述一个同时带有属性的函数,或者一个附带辅助方法的类,这仍然是最简洁的写法。

对于类,static 方法也能完成同样的工作,而且是纯 JavaScript。对于函数,你也可以不用命名空间,直接在声明之后赋值 format.prefix = "$";TypeScript 会追踪以这种方式赋值的属性。

命名空间与模块

在 ES 模块出现之前,大型 TypeScript 程序由许多共享全局命名空间的脚本文件组成,用 /// <reference path="..." /> 串联起来,再通过 outFile 编译成一个文件。模块取代了这种做法:每个文件都有自己的作用域,依赖是显式的导入,打包工具可以丢弃未使用的导出。TypeScript 7 移除了 outFile(错误 TS5102),所以多文件命名空间的方案已经不再是可选的构建方式。

命名空间模块
单位文件中的一个具名代码块文件本身
作用域全局,除非位于某个模块内始终独立
依赖隐式,靠加载顺序显式的 import
输出由函数构建的对象import/export 或 require
移除未使用的代码打包工具会保留每个成员打包工具可以丢弃未使用的导出
能否在 Node 类型剥离下运行只有只包含类型时才行可以

在模块内部,把所有内容都包进一个命名空间只会平白多出一层命名:导入方得写成 Utils.Utils.format。直接导出函数,如果导入方想要前缀,就让他们自己写 import * as Utils from "./utils.js"。

你仍会遇到命名空间的地方

声明文件用它们来描述只暴露一个全局对象的库,以及对类型进行分组:

// jquery-like.d.ts: a global function that also has properties
declare function $(selector: string): unknown;
declare namespace $ {
    const version: string;
    function ajax(url: string): Promise<unknown>;
}

类型包用它们来暴露可供你扩展的类型。@types/node 声明了 namespace NodeJS,要在任意模块中扩充它的 ProcessEnv 接口,需要用 declare global:

// env.d.ts
export {};

declare global {
    namespace NodeJS {
        interface ProcessEnv {
            API_URL: string; // process.env.API_URL is now string, not string | undefined
        }
    }
}

合并的命名空间中的接口也会合并,这种扩充正是靠这一点生效的。

命名空间与类型剥离

Node 24 通过删除类型语法来运行 .ts 文件。带有值的命名空间无法被直接删除,必须编译成上面展示的对象,所以 node app.ts 会停止并报错:

SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript namespace declaration is not supported in strip-only mode

有两种命名空间没问题,因为它们会完全消失:declare namespace,以及所有成员都是类型或接口的命名空间。编译选项 erasableSyntaxOnly: true 会在编译时把其他命名空间报告为错误 TS1294:This syntax is not allowed when 'erasableSyntaxOnly' is enabled.,依赖类型剥离运行的项目就是这样把它们挡在外面的。node --experimental-transform-types 确实会编译命名空间,但会给出实验性功能的警告。

常见问题

TypeScript 中应该用命名空间还是模块?

新代码用模块(import 和 export)。每个文件本身就是独立的作用域,打包工具和 Node 都理解模块,未使用的导出也可以被移除。命名空间在声明文件、全局扩充,以及给同名函数或类附加类型或辅助方法时仍然有用。

TypeScript 命名空间会编译成什么?

一个由立即调用函数填充的对象:var Geometry; (function (Geometry) { Geometry.circle = circle; })(Geometry || (Geometry = {}));。导出的成员成为这个对象的属性;没有 export 的成员只是这个函数的局部变量。

TypeScript 中 namespace 和 module 有什么区别?

模块是带有顶层 import 或 export 的文件,命名空间是文件内的一个具名代码块。早期的 TypeScript 把命名空间叫做“内部模块”,允许写 module Foo {};这种写法现在会报错 TS1540,只接受 namespace Foo {}。

Node 能运行使用了命名空间的 TypeScript 文件吗?

用它默认的类型剥离不行。包含值的命名空间会产生 ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX,因为剥离只会删除类型,而命名空间需要生成代码。只包含类型的命名空间以及 declare namespace 会被擦除,可以正常运行。

Coddy 编程语言插图

和 Coddy 一起学编程

开始