命名空间(namespace)
本教程共 80 篇 · 第 57 篇 · 更新于 2026-08-10 · 约 9 分钟阅读
本节目标:了解 TypeScript 命名空间的语法和设计意图,知道它怎么用、什么时候不该用,以及在现代 TypeScript 项目中它还有哪些合理场景。
namespace 是 TypeScript 从早期版本就有的组织代码方式。在 ES Module 标准落地之前,它是 TypeScript 解决”全局作用域污染”问题的主要手段。到了 2026 年,ES 模块已经无处不在,namespace 的角色变成了主要在声明文件(.d.ts)中使用的辅助工具。
从一个例子理解 namespace
假设你在写一个表单验证工具,有几个验证器类。如果不做任何组织,所有类都会暴露在全局:
class LettersOnlyValidator {
isAcceptable(s: string): boolean {
return /^[A-Za-z]+$/.test(s);
}
}
class ZipCodeValidator {
isAcceptable(s: string): boolean {
return s.length === 6 && /^\d+$/.test(s);
}
}
namespace 把它们包起来,加上一层命名隔离:
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^\d+$/;
export interface StringValidator {
isAcceptable(s: string): boolean;
}
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string): boolean {
return lettersRegexp.test(s);
}
}
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string): boolean {
return s.length === 6 && numberRegexp.test(s);
}
}
}
// 外部使用
const validator: Validation.StringValidator =
new Validation.ZipCodeValidator();
console.log(validator.isAcceptable("100000")); // true
注意几个关键点:lettersRegexp 和 numberRegexp 没有 export,所以只在 namespace 内部可见——它们是实现细节,外部访问不到。LettersOnlyValidator 和 ZipCodeValidator 有 export,所以通过 Validation. 前缀暴露出去。
编译成 JavaScript 后,namespace 变成了一个 IIFE(立即执行函数),export 的成员挂在 Validation 对象上:
var Validation;
(function (Validation) {
var lettersRegexp = /^[A-Za-z]+$/;
var numberRegexp = /^\d+$/;
var LettersOnlyValidator = /** @class */ (function () {
function LettersOnlyValidator() {}
LettersOnlyValidator.prototype.isAcceptable = function (s) {
return lettersRegexp.test(s);
};
return LettersOnlyValidator;
})();
Validation.LettersOnlyValidator = LettersOnlyValidator;
// ...
})(Validation || (Validation = {}));
嵌套 namespace
namespace 可以嵌套:
namespace Shapes {
export namespace Polygons {
export class Triangle {
constructor(public base: number, public height: number) {}
area(): number {
return (this.base * this.height) / 2;
}
}
export class Square {
constructor(public side: number) {}
area(): number {
return this.side * this.side;
}
}
}
}
const t = new Shapes.Polygons.Triangle(3, 4);
console.log(t.area()); // 6
多文件拆分
namespace 的一个”历史特色”是跨文件扩展——同一个 namespace 可以在多个文件中声明,TypeScript 编译时自动合并。这需要 /// <reference path=""> 指令来标记文件依赖:
// Validation.ts
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
}
// LettersOnlyValidator.ts
/// <reference path="Validation.ts" />
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string): boolean {
return lettersRegexp.test(s);
}
}
}
然后用 outFile 把所有文件合并成一个 JS 输出:
tsc --outFile dist/validation.js Validation.ts LettersOnlyValidator.ts
这套工作流在 2015 年以前很常见。但今天有了 ES Module,我们不需要手动管理文件依赖和合并顺序——打包工具和模块加载器会处理好一切。
为什么现代项目不推荐 namespace
TypeScript 官方文档的措辞很明确:推荐模块(ES Modules)而非命名空间。原因很实际:
1. 模块有独立作用域。 两个模块不会互相污染,namespace 归根结底还是全局对象的一个属性。
2. 模块依赖关系显式。 import 语句直接告诉你”这个文件依赖谁”,namespace 的 /// <reference> 藏在文件头里,容易被忽略。
3. 打包工具原生支持模块。 esbuild、webpack、Vite 生来就理解 import/export,可以做 tree-shaking、code splitting。namespace 对这些优化是盲区。
4. 避免 shapes.Shapes 式的尴尬。 有些从 namespace 迁移到模块的项目会写出:
// ❌ 多余的嵌套
export namespace Shapes {
export class Triangle {}
}
// 使用时:
import * as shapes from "./shapes";
new shapes.Shapes.Triangle(); // 为什么是 shapes.Shapes?
模块提供自然的命名隔离——import * as shapes 本身就给了命名空间,不需要再包一层 namespace。
namespace 的合理场景
不过 namespace 没有被废弃,在以下场景里它依然是最合适的工具:
1. 声明文件(.d.ts)里描述全局库。
jQuery、D3 这类通过 <script> 标签加载的库,API 挂在全局对象上。用 namespace 描述它们很自然:
// jquery.d.ts(简化版)
declare namespace jQuery {
interface Selector {
find(selector: string): Selector;
addClass(className: string): Selector;
}
function $(selector: string): Selector;
}
2. 声明合并——给已有的 class、function、enum 附加属性。
第 60 章会详细讲这个模式,但核心思路是:namespace 和 class/function/enum 可以同名合并,让你在类型层面表达”函数自身也有属性”这种 JS 里常见的写法。
// express(简化版)
declare function express(): express.Application;
declare namespace express {
interface Application {
get(path: string, handler: Function): void;
listen(port: number): void;
}
}
3. 在 .d.ts 文件里组织大型类型声明。
有时候模块的类型声明很复杂,用 namespace 给内部类型加一层分组能让 .d.ts 文件更好读。
小结
namespace 是 TypeScript 的”过去式”组织方式。新项目用 ES 模块,别用 namespace 来组织代码。namespace 的主要活跃场景在 .d.ts 声明文件里——描述全局库、实现声明合并、组织复杂类型声明。如果你打开一个现代 TypeScript 项目的源码目录看到满屏 namespace,那要么是遗产代码,要么是声明文件。