声明合并

可以在类型层面上描述JavaScript对象的模型

声明合并指指编译器将针对同一个名字的两个独立声明合并为单一声明。 合并后的声明同时拥有原先两个声明的特性。 任何数量的声明都可被合并;不局限于两个声明

基础概念

TypeScript中的声明会创建以下三种实体之一:命名空间,类型或值

创建命名空间的声明会新建一个命名空间,它包含了用(.)符号来访问时使用的名字。 创建类型的声明是:用声明的模型创建一个类型并绑定到给定的名字上。 最后,创建值的声明会创建在JavaScript输出中看到的值。

Declaration Type Namespace Type Value
Namespace X X
Class X X
Enum X X
Interface X
Type Alias X
Function X
Variable X

合并接口

从根本上说,合并的机制是把双方的成员放到一个同名的接口里

1
2
3
4
5
6
7
8
9
10
interface Box {
height: number;
width: number;
}

interface Box {
scale: number;
}

let box: Box = {height: 5, width: 6, scale: 10};

接口的非函数的成员应该是唯一的,如果不唯一,则必须是相同的类型

对于函数成员,每个同名函数声明都会被当成这个函数的一个重载。合并时后面的接口具有更高的优先级,即后来的接口重载会出现在考前的位置。有一个例外就是,如果签名里有一个参数的类型是_单一_的字符串字面量(比如,不是字符串字面量的联合类型),那么它将会被提升到重载列表的最顶端

1
2
3
4
5
6
7
8
9
10
11
interface Document {
createElement(tagName: any): Element;
}
interface Document {
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
}
interface Document {
createElement(tagName: string): HTMLElement;
createElement(tagName: "canvas"): HTMLCanvasElement;
}
1
2
3
4
5
6
7
interface Document {
createElement(tagName: "canvas"): HTMLCanvasElement;
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
createElement(tagName: string): HTMLElement;
createElement(tagName: any): Element;
}

合并命名空间

同名的命名空间也会合并其成员,命名空间会创建出命名空间和值

对于命名空间的合并,模块导出的同名接口进行合并,构成单一命名空间内含合并后的接口

对于命名空间里值的合并,如果当前已经存在给定名字的命名空间,那么后来的命名空间的导出成员会被加到已经存在的那个模块里

合并之后,从其它命名空间合并进来的成员无法访问非导出成员

命名空间与类和函数和枚举类型合并

合并命名空间和类

1
2
3
4
5
6
class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel { }
}

合并结果是一个类并带有一个内部类。需要将namespace中的类先导出。

命名空间与函数合并

用于稍后对函数扩展,并保证类型安全

1
2
3
4
5
6
7
8
9
10
function buildLabel(name: string): string {
return buildLabel.prefix + name + buildLabel.suffix;
}

namespace buildLabel {
export let suffix = "";
export let prefix = "Hello, ";
}

console.log(buildLabel("Sam Smith"));

命名空间与枚举类合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum Color {
red = 1,
green = 2,
blue = 4
}

namespace Color {
export function mixColor(colorName: string) {
if (colorName == "yellow") {
return Color.red + Color.green;
}
else if (colorName == "white") {
return Color.red + Color.green + Color.blue;
}
else if (colorName == "magenta") {
return Color.red + Color.blue;
}
else if (colorName == "cyan") {
return Color.green + Color.blue;
}
}
}

非法的合并

类不能与其它类或变量合并,可以通过混入实现类似效果。

模块扩展

指的就是 declare。当引入其他模块后对模块进行补丁式的扩展时,编译器无法识别我们对其进行的扩展,这是可以通过 declare 来告诉编译器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// observable.ts
export class Observable<T> {
// ... implementation left as an exercise for the reader ...
}

// map.ts
import { Observable } from "./observable";
declare module "./observable" {
interface Observable<T> {
map<U>(f: (x: T) => U): Observable<U>;
}
}
Observable.prototype.map = function (f) {
// ... another exercise for the reader
}


// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map(x => x.toFixed());

模块名的解析和用import/export解析模块标识符的方式是一致的

当这些声明在扩展中合并时,就如同在原始位置被声明一样。 但是,有两点限制需要注意

  1. 你不能在扩展中声明新的顶级声明-仅可以扩展模块中已经存在的声明
  2. 默认导出也不能扩展,只有命名的导出才可以(因为你需要使用导出的名字来进行扩展,并且default是保留关键字

全局扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// observable.ts
export class Observable<T> {
// ... still no implementation ...
}

declare global {
interface Array<T> {
toObservable(): Observable<T>;
}
}

Array.prototype.toObservable = function () {
// ...
}

全局扩展与模块扩展的行为和限制是相同的

作者

徐云飞

发布于

2022-10-28

更新于

2023-02-05

许可协议

评论