首页 / TypeScript 入门教程 / 命名空间(namespace)

TypeScript 入门教程

命名空间(namespace)

本教程共 80 篇 · 第 57 篇 · 更新于 2026-08-10 · 约 9 分钟阅读

TypeScriptTypeScript 入门教程命名空间namespace声明文件

本节目标:了解 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

注意几个关键点:lettersRegexpnumberRegexp 没有 export,所以只在 namespace 内部可见——它们是实现细节,外部访问不到。LettersOnlyValidatorZipCodeValidatorexport,所以通过 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,那要么是遗产代码,要么是声明文件。