# 命名空间

除了采用模块语法组织逻辑,Typescript 也提供了其他语言中常见的 命名空间namespace 特性来组织代码。

# 命名空间语法

# 命名空间申明

采用

namespace NamespaceName
{ 
    NamespaceBody 
}

申明命名空间,NamespaceBody 支持所有合法的 Typescript 语法。

namespace A1 {
   // 这些类型和变量只在命名空间内可用
   var a = 1;
   interface Foo {
      a?: 1
   };

   function log() {
      console.log(a)
   }
}


命名空间的代码会被编译成 IIFE (opens new window) 的结构。

var <NamespaceName>;  
(function(<NamespaceName>) {  
    <NamespaceStatements>  
})(<NamespaceName>||(<NamespaceName>={}));
var A1;
(function (A1) {
    // 这些类型和变量只在命名空间内可用
    var a = 1;
    ;
    function log() {
        console.log(a);
    }
})(A1 || (A1 = {}));

# 命名空间实体(NamespaceBody)

由于命名空间生成的代码是通过创建一个 IIFE (opens new window) 的结构, 传入定义的名称实现。命名空间实体支持只包含语句。

var DEV = true;

namespace __DEV__ {
   if(DEV) {
      console.log('dev environment')
   }
}

更详细的信息参见 Namespace Body (opens new window)

# export 申明

未导出的内容只在命名空间内部可用,可采用模块语法,使用 export 进行导出。

namespace A1 {
   // 这些类型和变量只在命名空间内可用
   var a = 1;
   interface Foo {
      a?: 1
   };

   function log1() {
      console.log(a)
   }


   export function log() {
      console.log(a)
   }

}

// 合法
A1.log()

// 报错,该方法未导出无法使用
A1.log1()

上述代码被编译为如下结果

var A1;
(function (A1) {
    // 这些类型和变量只在命名空间内可用
    var a = 1;
    ;
    function log1() {
        console.log(a);
    }
    function log() {
        console.log(a);
    }
    A1.log = log;
})(A1 || (A1 = {}));
// 合法
A1.log();
// 报错,该方法未导出无法使用
A1.log1();

导出内容通过 A1.log = log 的方式暴露给外部。

若导出内容依赖于内部类型,则内部类型也建议导出。


namespace M3 {  
    // 由于导出函数依赖类型 C, C 依赖类型 B 建议导出
    export type B = 'a' | 'b'  
    
    // 由于导出函数依赖类型 C,建议导出
    export interface C { x: B; }  

    export function fooA1(c: C) { 
       console.log(c)
    }  
}

M3.fooA1({x: 'a'} )

TIP

参考 typescript 规范 命名空间导出 (opens new window) 的导出依赖为强限制,实际使用未发现此问题,但还是建议在使用时导出依赖类型。

由于命名空间的编译结果就是变量,你也可以将命名空间作为模块导出。

A.ts
export namespace A {
  export var a = 1;
}
B.ts
// 注意虽然和 A.ts 中模块同名,但是由于导出为模块作用域只在模块内有效
export namespace A {
  export var c = 1;
}

export namespace B {
  export var Foo = 1;
}

采用 import 导入命名空间

import * as A from './A'
import * as B from './B'

console.log(A)
console.log(B)

# 嵌套

命名空间实体内部也支持命名空间语法,可以嵌套申明多个命名空间

namespace A1 {
   export namespace B {
      export   namespace C {
         export interface A {
            a?: number
         }

         export var a =  1;
         export function log() {
            console.log('log')
         }
      }

   }
}


let a111 = A1.B.C.a
let abc1:A1.B.C.A;
A1.B.C.log();

上述定义也可采用形如 <namespace1>.<namespace2>.<namespace3>... 的形式创建

namespace A1.B.C {
   export interface A {
      a?: number
   }

   export var a =  1;
   export function log() {
      console.log('log')
   }
}


let a111 = A1.B.C.a
let abc1:A1.B.C.A;
A1.B.C.log();

注意此方式你无需显示申明 export 导出内层命名空间,采用 <namespace1>.<namespace2>.<namespace3>... 的方式申明时,内层命名空间 namespace2 和 namespace3 默认是可导出的。

你也可以组合多层命名空间定义各自的内容。

namespace A1 {
   // 定义 A1 命名空间的内容
   export var a = 1;

   export namespace B.C {
      export interface A {
         a?: number
      }

      export var a =  1;
      export function log() {
         console.log('log', A1.a)
      }
   }
   namespace F {
      export function log() {
         console.log('F namespace')
      }
   }
   // 你直接在 A1 的命名空间内部使用内层命名空间
   B.C.log()

   // 即使命名空间未导出,但是可获取该命名空间内部导出方法
   F.log()

}


let a111 = A1.B.C.a
let abc1:A1.B.C.A;
A1.B.C.log();
console.log(A1)

# 别名

若命名空间过长可使用 import <AliasName> = <NamespaceName> 方式定义别名。

namespace LongNameSpaceName {
   export const a = 1
}

namespace LongNameSpaceName1 {
   export type A = 'a' | 'b'
}

// 采用 import 简化长命名空间导入
import LN = LongNameSpaceName;
console.log(LN.a)

TIP

由于命名空间不是类型定义,所以不能使用 type 语法来定义别名。

别名会导入引用的命名空间的所有 export 的值和类型。

namespace A1 {  
   export interface X { s: string }  
   export var X: X;  
}

namespace B {  
   // 注意此处赋值只包含 A1 命名空间的值,不包含类型
   var Y = A1;    // Alias for namespace A  
   // 此处会报错,应为 Y 不包含导入命名空间的类型
   var B: Y.X;


    // import 不仅创建了别名,同时导入了 A1 所有 export 的值和类型
   import Y1 = A1;    
   // 合法,由于导入了所有申明,所以包含 A1 的导出类型
   var B1: Y1.X;
}

# 命名空间合并

若命名空间与外部函数或类同名,导出内容会合并到同名的函数和类中。

函数的合并

interface Point {  
   x: number;  
   y: number;  
}

function point(x: number, y: number): Point {  
   return { x: x, y: y };  
}

namespace point {  
   // 导出内容会合并到函数 point 函数作为属性
   export var origin = point(0, 0);  
   export function equals(p1: Point, p2: Point) {  
       return p1.x == p2.x && p1.y == p2.y;  
   }  
}

var p1 = point(0, 0);  
var p2 = point.origin;  
var b = point.equals(p1, p2);
console.log(point)

类的合并

class C {}
namespace C {
   // 导出内容会合并到类 C 作为静态成员和方法
   export var b = 1;
   export function log() {

   }
}

console.log(C)

对于默认导出的函数,不支持命名空间合并

export default function foo() {
}

// 非法
namespace foo {
    var x = 100;
}

详见 Function and class default export declarations can no longer merge with entities intersecting in their meaning (opens new window)

命名空间也不支持变量的合并

var a = {}
// 该合并会报错
namespace a {
   // 导出内容会合并到类 C 作为静态成员和方法
   export var b = 1;
   export function log() {

   }
}

console.log(a)

你可以阅读如下 issue 了解原因

多个文件可以申明相同的命名空间,例如创建一个校验库。库中多个校验文件,每个文件申明特定的校验对象。

StringValidator.ts
namespace Validation {
  export interface StringValidator {
    isAcceptable(s: string): boolean;
  }
}
LettersOnlyValidator.ts
namespace Validation {
  const lettersRegexp = /^[A-Za-z]+$/;
  export class LettersOnlyValidator1 implements StringValidator {
    isAcceptable(s: string) {
      return lettersRegexp.test(s);
    }
  }
}
ZipCodeValidator.ts
namespace Validation {
  const numberRegexp = /^[0-9]+$/;
  export class ZipCodeValidator1 implements StringValidator {
    isAcceptable(s: string) {
      return s.length === 5 && numberRegexp.test(s);
    }
  }
}

在 app.ts 可以直接使用

let strings1 = ['Hello', '98052', '101'];

let validators1: { [s: string]: Validation.StringValidator } = {};
validators1['ZIP code'] = new Validation.ZipCodeValidator1();
validators1['Letters only'] = new Validation.LettersOnlyValidator1();

for (let s of strings1) {
  for (let name in validators1) {
    console.log(
      `"${s}" - ${
        validators1[name].isAcceptable(s) ? 'matches' : 'does not match'
      } ${name}`
    );
  }
}

为了在编译时可使用命名空间,需采用 tsc --outFile app.js StringValidator.ts LettersOnlyValidator.ts ZipCodeValidator.ts app.ts 将多个命名空间组合在一起

也可使用 /// <reference> 指令显示申明依赖的命名空间

/// <reference path="StringValidator.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />

let strings1 = ['Hello', '98052', '101'];

let validators1: { [s: string]: Validation.StringValidator } = {};
validators1['ZIP code'] = new Validation.ZipCodeValidator1();
validators1['Letters only'] = new Validation.LettersOnlyValidator1();

for (let s of strings1) {
  for (let name in validators1) {
    console.log(
      `"${s}" - ${
        validators1[name].isAcceptable(s) ? 'matches' : 'does not match'
      } ${name}`
    );
  }
}

注意多个文件采用相同命名空间会导致产生多个闭包函数,注入方法。并且配置也更复杂。, 参看 Needless Namespacing (opens new window) 进一步理解采用模块替换命名空间的方法。

# 深入理解命名空间

# 实例化

命名空间分为两种

  • 非实例化命名空间(non-instantiated.) 只包含类型申明和非实例化命名空间
  • 实例化命名空间(instantiated) 包含实际值和逻辑定义的命名空间

具体而言实例化命名空间文件会编译生成 js 代码,而非实例化命名空间由于只包含类型等信息,编译输出为空。

该段代码由于不包含值定义,编译输出为空。

// 命名空间 A1 只包含类型定义,所以编译输出 js 为空
namespace A1 {
    interface A1 {
        a: string
    }
    namespace B1 {
        type C = string;
    }
}

理解该概念可以解释如下现象。

1. 非实例命名空间不产生实际代码,所以无法作为值使用。

namespace A1 {
    interface A1 {
        a: string
    }
    namespace B1 {
        type C = string;
    }
}

// A 为非实例命名空间,无法作为值传递给变量,所以编译会报错
let b = A1;

2. 实例命名空间进行复制操作时只传递值

这也可以解释之前 别名 小节中需要使用 import 语法导入命名空间,而赋值操作无法使用命名空间导出类型的问题。

# module 和 namespace

参考 规范命名空间申明 (opens new window),官方表示早期版本的 module 关键字向后兼容 namespace 语义。

module M {  
    let a = 1
    export function foo() { 

    }  
}

// namespace 替换 module 关键字,语义相同
namespace M1 {  
    let a = 1
    export function foo() { 

    }  
}

在 Typescript 实现 ES6 模块语法之前,采用 module 进行模块定义, 在版本 1.5 (opens new window) 替换为 namespace 表示模块概念。为了区分 Typescript 的模块语义和 ES6 的模块定义。 Typescript 规定

  • 内部模块(internal module) 表示 Typescript 模块语义,采用 module 或 namespace 关键字
  • 外部模块(external module) 表示 ES6, commonjs 等 js 生态的模块语义

你可以阅读 How do I use namespaces with TypeScript external modules (opens new window) 进一步理解命名空间如何和模块系统一起使用。

# 如何正确使用命名空间

当前模块系统是以文件为粒度拆分功能,而命名空间提供了另一个维度的代码拆分能力。不仅可以在单一文件中通过命名空间拆分多个子模块,同时可以组合多个文件为单一模块(相比采用模块 import 方式,命名空间语义更直观)。你可以阅读如下资料理解命名空间的使用。

# 知识点

本章讲解了除了模块语法外,采用命名空间组织代码逻辑的方法。Typescript 通过将命名空间编译为 IIFE (opens new window) 函数实现隔离。核心知识点为

  1. 采用 namespace 关键字声明命名空间,可以用早期版本 module 关键字进行替换。

    namespace NamespaceName
    { 
        NamespaceBody 
    }
    
    • NamespaceName 支持 <namespace1>.<namespace2> 的方式定义嵌套的命名空间,也可直接申明嵌套命名空间
    • NamespaceBody 支持几乎所有 Typescript 语法,甚至可以只包含执行逻辑
  2. 利用模块关键字 export 导出命名空间内部申明

  3. 采用 import <aliasName> = <NamespaceName> 方式设置命名空间别名,该语法会导入空间内可导出的所有值和类型定义。

  4. 命名空间会和外层同名的函数或类定义产生合并。可以将命名空间分散定义多个文件。通过 /// <reference> 指令申明依赖,或编译组合多个文件方式使用。

  5. 命名空间根据是否产出实际代码分为

    1. 实例化命名空间
    2. 非实例化命名空间 只有类型定义,编译产生代码为空