# 命名空间
除了采用模块语法组织逻辑,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;
}命名空间也不支持变量的合并
var a = {}
// 该合并会报错
namespace a {
// 导出内容会合并到类 C 作为静态成员和方法
export var b = 1;
export function log() {
}
}
console.log(a)你可以阅读如下 issue 了解原因
- declaration: Namespace which exports class can't be merged with variable (opens new window)
- Merging constants with namespaces (opens new window)
多个文件可以申明相同的命名空间,例如创建一个校验库。库中多个校验文件,每个文件申明特定的校验对象。
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) 函数实现隔离。核心知识点为
采用
namespace关键字声明命名空间,可以用早期版本module关键字进行替换。namespace NamespaceName { NamespaceBody }NamespaceName支持<namespace1>.<namespace2>的方式定义嵌套的命名空间,也可直接申明嵌套命名空间NamespaceBody支持几乎所有 Typescript 语法,甚至可以只包含执行逻辑
利用模块关键字
export导出命名空间内部申明采用
import <aliasName> = <NamespaceName>方式设置命名空间别名,该语法会导入空间内可导出的所有值和类型定义。命名空间会和外层同名的函数或类定义产生合并。可以将命名空间分散定义多个文件。通过
/// <reference>指令申明依赖,或编译组合多个文件方式使用。命名空间根据是否产出实际代码分为
- 实例化命名空间
- 非实例化命名空间 只有类型定义,编译产生代码为空