TypeScript namespace

namespace 是一种将相关代码组织在一起的方式,中文译为“命名空间”。

它出现在 ES 模块诞生之前,作为 TypeScript 自己的模块格式而发明的。但是,自从有了 ES 模块,官方已经不推荐使用 namespace 了。

基本用法

namespace 用来建立一个容器,内部的所有变量和函数,都必须在这个容器里面使用。

  1. namespace Utils {
  2. function isString(value:any) {
  3. return typeof value === 'string';
  4. }
  5. // 正确
  6. isString('yes');
  7. }
  8. Utils.isString('no'); // 报错

上面示例中,命名空间Utils里面定义了一个函数isString(),它只能在Utils里面使用,如果用于外部就会报错。

如果要在命名空间以外使用内部成员,就必须为该成员加上export前缀,表示对外输出该成员。

  1. namespace Utility {
  2. export function log(msg:string) {
  3. console.log(msg);
  4. }
  5. export function error(msg:string) {
  6. console.error(msg);
  7. }
  8. }
  9. Utility.log('Call me');
  10. Utility.error('maybe!');

上面示例中,只要加上export前缀,就可以在命名空间外部使用内部成员。

编译出来的 JavaScript 代码如下。

  1. var Utility;
  2. (function (Utility) {
  3. function log(msg) {
  4. console.log(msg);
  5. }
  6. Utility.log = log;
  7. function error(msg) {
  8. console.error(msg);
  9. }
  10. Utility.error = error;
  11. })(Utility || (Utility = {}));

上面代码中,命名空间Utility变成了 JavaScript 的一个对象,凡是export的内部成员,都成了该对象的属性。

这就是说,namespace 会变成一个值,保留在编译后的代码中。这一点要小心,它不是纯的类型代码。

namespace 内部还可以使用import命令输入外部成员,相当于为外部成员起别名。当外部成员的名字比较长时,别名能够简化代码。

  1. namespace Utils {
  2. export function isString(value:any) {
  3. return typeof value === 'string';
  4. }
  5. }
  6. namespace App {
  7. import isString = Utils.isString;
  8. isString('yes');
  9. // 等同于
  10. Utils.isString('yes');
  11. }

上面示例中,import命令指定在命名空间App里面,外部成员Utils.isString的别名为isString

import命令也可以在 namespace 外部,指定别名。

  1. namespace Shapes {
  2. export namespace Polygons {
  3. export class Triangle {}
  4. export class Square {}
  5. }
  6. }
  7. import polygons = Shapes.Polygons;
  8. // 等同于 new Shapes.Polygons.Square()
  9. let sq = new polygons.Square();

上面示例中,import命令在命名空间Shapes的外部,指定Shapes.Polygons的别名为polygons

namespace 可以嵌套。

  1. namespace Utils {
  2. export namespace Messaging {
  3. export function log(msg:string) {
  4. console.log(msg);
  5. }
  6. }
  7. }
  8. Utils.Messaging.log('hello') // "hello"

上面示例中,命名空间Utils内部还有一个命名空间Messaging。注意,如果要在外部使用Messaging,必须在它前面加上export命令。

使用嵌套的命名空间,必须从最外层开始引用,比如Utils.Messaging.log()

namespace 不仅可以包含实义代码,还可以包括类型代码。

  1. namespace N {
  2. export interface MyInterface{}
  3. export class MyClass{}
  4. }

上面代码中,命令空间N不仅对外输出类,还对外输出一个接口,它们都可以用作类型。

namespace 与模块的作用是一致的,都是把相关代码组织在一起,对外输出接口。区别是一个文件只能有一个模块,但可以有多个 namespace。由于模块可以取代 namespace,而且是 JavaScript 的标准语法,还不需要编译转换,所以建议总是使用模块,替代 namespace。

如果 namespace 代码放在一个单独的文件里,那么引入这个文件需要使用三斜杠的语法。

  1. /// <reference path = "SomeFileName.ts" />

namespace 的输出

namespace 本身也可以使用export命令输出,供其他文件使用。

  1. // shapes.ts
  2. export namespace Shapes {
  3. export class Triangle {
  4. // ...
  5. }
  6. export class Square {
  7. // ...
  8. }
  9. }

上面示例是一个文件shapes.ts,里面使用export命令,输出了一个命名空间Shapes

其他脚本文件使用import命令,加载这个命名空间。

  1. // 写法一
  2. import { Shapes } from './shapes';
  3. let t = new Shapes.Triangle();
  4. // 写法二
  5. import * as shapes from "./shapes";
  6. let t = new shapes.Shapes.Triangle();

不过,更好的方法还是建议使用模块,采用模块的输出和输入。

  1. // shapes.ts
  2. export class Triangle {
  3. /* ... */
  4. }
  5. export class Square {
  6. /* ... */
  7. }
  8. // shapeConsumer.ts
  9. import * as shapes from "./shapes";
  10. let t = new shapes.Triangle();

上面示例中,使用模块的输出和输入,改写了前面的例子。

namespace 的合并

多个同名的 namespace 会自动合并,这一点跟 interface 一样。

  1. namespace Animals {
  2. export class Cat {}
  3. }
  4. namespace Animals {
  5. export interface Legged {
  6. numberOfLegs: number;
  7. }
  8. export class Dog {}
  9. }
  10. // 等同于
  11. namespace Animals {
  12. export interface Legged {
  13. numberOfLegs: number;
  14. }
  15. export class Cat {}
  16. export class Dog {}
  17. }

这样做的目的是,如果同名的命名空间分布在不同的文件中,TypeScript 最终会将它们合并在一起。这样就比较方便扩展别人的代码。

合并命名空间时,命名空间中的非export的成员不会被合并,但是它们只能在各自的命名空间中使用。

  1. namespace N {
  2. const a = 0;
  3. export function foo() {
  4. console.log(a); // 正确
  5. }
  6. }
  7. namespace N {
  8. export function bar() {
  9. foo(); // 正确
  10. console.log(a); // 报错
  11. }
  12. }

上面示例中,变量a是第一个名称空间N的非对外成员,它只在第一个名称空间可用。

命名空间还可以跟同名函数合并,但是要求同名函数必须在命名空间之前声明。这样做是为了确保先创建出一个函数对象,然后同名的命名空间就相当于给这个函数对象添加额外的属性。

  1. function f() {
  2. return f.version;
  3. }
  4. namespace f {
  5. export const version = '1.0';
  6. }
  7. f() // '1.0'
  8. f.version // '1.0'

上面示例中,函数f()与命名空间f合并,相当于命名空间为函数对象f添加属性。

命名空间也能与同名 class 合并,同样要求class 必须在命名空间之前声明,原因同上。

  1. class C {
  2. foo = 1;
  3. }
  4. namespace C {
  5. export const bar = 2;
  6. }
  7. C.bar // 2

上面示例中,名称空间C为类C添加了一个静态属性bar

命名空间还能于同名 Enum 合并。

  1. enum E {
  2. A,
  3. B,
  4. C,
  5. }
  6. namespace E {
  7. export function foo() {
  8. console.log(E.C);
  9. }
  10. }
  11. E.foo() // 2

上面示例中,命名空间E为枚举E添加了一个foo()方法。

注意,Enum 成员与命名空间导出成员不允许同名。

  1. enum E {
  2. A, // 报错
  3. B,
  4. }
  5. namespace E {
  6. export function A() {} // 报错
  7. }

上面示例中,同名 Enum 与命名空间有同名成员,结果报错。