定义合并
状态: 初稿
介绍
TypeScript 一些独特概念在类型层面描述 JavaScript 对象的形体.
这其中尤其特殊的一个便是 TypeScript ‘定义合并’ 的概念.
理解此概念不仅有助于你与现有 JavaScript 代码协作.
它同时打开了通往更高级抽象的大门.
在本章, “定义合并”特指编译器把两个名字相同但各自独立的定义合并到一起.
合并后的定义具有来自两个独立定义的所有功能.
任何数量的定义都能合并; 不限于两个.
基础知识
在 TypeScript 中, 每个声明创建的实体都归于以下三组: 名字空间, 类型, 和值.
名字空间声明创建一个名字空间, 它包含通过点运算符才能访问的标识符.
类型声明创建形体符合声明的类型并绑定到给定名字上.
最后, 值声明创建在输出 JavaScript 可见的值.
声明类型 | 名字空间 | 类型 | 值 |
---|---|---|---|
名字空间 | X | X | |
类 | X | X | |
枚举 | X | X | |
接口 | X | ||
类型别名 | X | ||
函数 | X | ||
变量 | X |
理解每种声明创建了什么是理解定义合并合并哪些东西的前提.
接口合并
最简单, 可能也是最常见的定义合并是接口合并.
作为最基础的一级, 接口合并机械地把两个接口的成员加入名字相同的单个接口.
1 | interface Box { |
各个接口的非函数成员必须唯一.
如果它们不唯一(译注: 名字), 就要求类型一致.
如果两接口声明了名字相同, 但类型不同的非函数成员, 编译器会报错.
对函数成员, 每个名字相同的函数成员都视为是对同一函数重载的描述.
同时值得提醒, 假定接口 A
与后来的 A
合并, 第二个接口优先级更高.
下例体现这点:
1 | interface Cloner { |
这三个接口会合并, 产生如下单一接口声明:
1 | interface Cloner { |
可以看到每个组成员顺序保持不变, 但组本身按照越后(来)的组越靠前的顺序合并.
此规则唯一例外是特化签名.
如果一个签名有单个字符串字面量类型(例如: 不是字符串字面量自适应类型)的参数, 那么这个签名会浮动到合并后的重载签名列表最上方.
举个例子, 以下三个接口会合并:
1 | interface Document { |
Document
接口合并后结果如下:
1 | interface Document { |
名字空间合并
与接口一样, 名字相同的名字空间也会合并它们的成员.
由于名字空间声明同时创建名字空间和值, 我们需要理解两者分别是怎样合并的.
要合并名字空间中的名字空间, 来自在每个名字空间中声明的导出名字空间的类型定义自行合并, 形成一个包含合并后的接口定义的单一名字空间.
要合并名字空间中的值, 在每个声明点, 如果给定名字的名字空间已存在, 它会进一步接收已存在的名字空间, 添加它的导出成员以扩充自身.
下例是名字空间 Animals
的定义合并:
1 | namespace Animals { |
等同于:
1 | namespace Animals { |
理解这种名字空间合并模型是一个不错的开始, 但我们有必要了解非导出成员去了哪里.
非导出成员只在源(未经合并的)名字空间可见. 意味着合并以后, 来自其他声明的合并成员不能看见一个源名字空间的非导出成员.
下例更清晰地显示这点:
1 | namespace Animal { |
因为 haveMuscles
没有被导出, 只有共享同一未合并名字空间的 animalsHaveMuscles
函数可以看到这个标识符.
即使 doAnimalsHaveMuscles
函数是合并后 Animal
名字空间的一部分, 也无法看到这个非导出成员.
名字空间与类, 函数, 枚举合并
名字空间足够灵活, 以至于可以与其他类型的声明合并.
它是前提条件是, 名字空间声明必须尾随待合并声明. 结果声明具有两种声明类型的属性.
TypeScript 借助此能力模拟 JavaScript 和其他语言中的一些模式.
与类合并
与类合并赋予用户以表达内部类的方法.
1 | class Album { |
合并成员的可见性规则与在’名字空间合并’一节描述的一致, 为了对合并后的类可见, 我们必须导出 AlbumLabel
类.
最终结果是管理内部另一个类的类.
你也可以用名字空间为现有类添加更多静态成员.
除内部类外, 你或许熟悉 JavaScript 创建一个函数然后通过为函数添加属性扩充它的做法.
TypeScript 借助定义合并以类型安全的形式构建这样的定义.
1 | function buildLabel(name: string): string { |
同样, 名字空间可以为枚举添加静态成员:
1 | enum Color { |
不允许的合并
并不是所有可能的合并都被允许.
目前, 类不能与其他类, 或变量合并.
想获取模拟类合并的信息, 参看TypeScript 中的 Minxin一章.
模块增强
虽然 JavaScript 模块不支持合并, 但你可以通过导入, 更新它们来补充现有对象.
请看这个朴素的 Observable 类:
1 | // observable.ts |
这是合法的 TypeScript 程序, 但编译器还不了解其中的 Observable.prototype.map
.
你可以利用模块增强告知编译器更多信息:
1 | // observable.ts |
模块名按照与 import
/export
模块标识符相同的方式解析.
阅读模块一节获得更多信息.
于是增强中的声明被合并, 如同它们直接声明在源文件中.
模块有两条需牢记的限制:
- 你不可以在增强中声明新的顶层声明 — 只能补充现有声明.
- 默认导出也不能增强, 只能增强命名导出 (因为导出名对增强来说是不可少的, 而
default
是个关键字 - 查看 #14080 了解细节)
全局增强
你可以在模块内部往全局空间添加声明:
1 | // observable.ts |
全局增强存在与模块增强一样的行为和限制.