命名空间是一个具名代码块,把函数、常量和类型归到一个名称下。标记了 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 会被擦除,可以正常运行。