Skip to content

Commit 4aa3242

Browse files
committed
translate declaration merging
1 parent 2f42931 commit 4aa3242

File tree

2 files changed

+282
-1
lines changed

2 files changed

+282
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
* [引用](https://github.com/Chorer/TypeScript-Doc-Zh/blob/main/zh/Reference)
3636
- [x] [工具类型](https://github.com/Chorer/TypeScript-Doc-Zh/blob/main/zh/Reference/Utility%20Types.md)
3737
- [ ] [装饰器](https://github.com/Chorer/TypeScript-Doc-Zh/blob/main/zh/Reference/Decorators.md)
38-
- [ ] [声明合并](https://github.com/Chorer/TypeScript-Doc-Zh/blob/main/zh/Reference/Declaration%20Merging.md)
38+
- [x] [声明合并](https://github.com/Chorer/TypeScript-Doc-Zh/blob/main/zh/Reference/Declaration%20Merging.md)
3939
- [ ] [枚举](https://github.com/Chorer/TypeScript-Doc-Zh/blob/main/zh/Reference/Enums.md)
4040
- [ ] [迭代器和生成器](https://github.com/Chorer/TypeScript-Doc-Zh/blob/main/zh/Reference/Iterators%20and%20Generators.md)
4141
- [ ] [JSX](https://github.com/Chorer/TypeScript-Doc-Zh/blob/main/zh/Reference/JSX.md)

zh/Reference/Declaration Merging.md

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
## 声明合并
2+
3+
### 介绍
4+
5+
TypeScript 中的某些独有概念在类型层面描述了 JavaScript 对象的形状。其中一个非常独有的概念就是“声明合并”。理解这个概念可以帮助你在处理现有的 JavaScript 时更加得心应手,同时,它也开启了通往更高级的抽象概念的大门。
6+
7+
对于本文而言,“声明合并”指的是编译器会将分开的两个相同名字的声明合并为一个声明。合并后的声明同时具有合并前声明的所有特性。任意数量的声明都可以合并到一起,不止局限于两个声明。
8+
9+
### 基础概念
10+
11+
在 TypeScript 中,一个声明创建的实体至少是下面三种类型的其中一种:命名空间、类型或者值。命名空间式的声明会创建一个命名空间,它包含的名字可以通过点访问符进行访问。类型式的声明会创建一个对已声明形状可见的类型,并且会绑定到给定的名字上。最后,值式的声明会创建在输出的 JavaScript 中可见的值。
12+
13+
| Declaration Type | Namespace | Type | Value |
14+
| :--------------: | :-------: | :--: | :---: |
15+
| Namespace | X | | X |
16+
| Class | | X | X |
17+
| Enum | | X | X |
18+
| Interface | | X | |
19+
| Type Alias | | X | |
20+
| Function | | | X |
21+
| Variable | | | X |
22+
23+
理解每个声明会创建什么,才能更好地理解进行声明合并时会合并什么。
24+
25+
### 合并接口
26+
27+
最简单、并且可能是最常见的声明合并类型就是接口合并。在大多数情况下,合并操作会机械地将两个声明的所有成员放到一个同名的接口中。
28+
29+
```ts
30+
interface Box {
31+
height: number;
32+
width: number;
33+
}
34+
interface Box {
35+
scale: number;
36+
}
37+
let box: Box = { height: 5, width: 6, scale: 10 };
38+
```
39+
40+
接口的非函数成员应该是唯一的。如果它们不是唯一的,那么类型必须相同。如果两个接口都声明了一个同名但不同类型的非函数成员,那么编译器会抛出一个错误。
41+
42+
对于函数类型的成员,每一个同名的函数成员都会被视为是同个函数的一个重载。还需要注意的是,如果前面的接口 `A` 和后面的接口 `A` 合并,那么第二个接口的优先级会比第一个接口高。
43+
44+
举个例子:
45+
46+
```ts
47+
interface Cloner {
48+
clone(animal: Animal): Animal;
49+
}
50+
interface Cloner {
51+
clone(animal: Sheep): Sheep;
52+
}
53+
interface Cloner {
54+
clone(animal: Dog): Dog;
55+
clone(animal: Cat): Cat;
56+
}
57+
```
58+
59+
这三个接口将会合并为单个声明,如下所示:
60+
61+
```ts
62+
interface Cloner {
63+
clone(animal: Dog): Dog;
64+
clone(animal: Cat): Cat;
65+
clone(animal: Sheep): Sheep;
66+
clone(animal: Animal): Animal;
67+
}
68+
```
69+
70+
注意,每个接口里面的成员都会保持原有的顺序,但是接口原先越靠前,在重载中的位置就越靠后。
71+
72+
这个规则有一个例外,那就是使用专有签名的时候。如果某个签名的参数类型是一个单独的字符串字面量类型(而不是字符串字面量的联合类型),那么这个签名会“冒泡”到达合并后的重载列表的顶端。
73+
74+
举个例子,下面的接口会进行合并:
75+
76+
```ts
77+
interface Document {
78+
createElement(tagName: any): Element;
79+
}
80+
interface Document {
81+
createElement(tagName: "div"): HTMLDivElement;
82+
createElement(tagName: "span"): HTMLSpanElement;
83+
}
84+
interface Document {
85+
createElement(tagName: string): HTMLElement;
86+
createElement(tagName: "canvas"): HTMLCanvasElement;
87+
}
88+
```
89+
90+
合并后的 `Document` 声明如下所示:
91+
92+
```ts
93+
interface Document {
94+
createElement(tagName: "canvas"): HTMLCanvasElement;
95+
createElement(tagName: "div"): HTMLDivElement;
96+
createElement(tagName: "span"): HTMLSpanElement;
97+
createElement(tagName: string): HTMLElement;
98+
createElement(tagName: any): Element;
99+
}
100+
```
101+
102+
### 合并命名空间
103+
104+
和接口类似,同名的命名空间的所有成员也会合并到一起。由于命名空间会创建命名空间和值,所以我们需要理解它们各自是怎么合并的。
105+
106+
对于命名空间的合并,在每个命名空间中声明的导出接口的类型定义自身会进行合并,形成一个单独的、包含合并后的接口定义的命名空间。
107+
108+
对于命名空间中的值的合并,如果给定名字的命名空间已经存在了,那么它会进行拓展,即接受已有的命名空间,同时将第二个命名空间的导出成员添加到第一个命名空间中。
109+
110+
下面例子中的 `Animals`
111+
112+
```ts
113+
namespace Animals {
114+
export class Zebra {}
115+
}
116+
namespace Animals {
117+
export interface Legged {
118+
numberOfLegs: number;
119+
}
120+
export class Dog {}
121+
}
122+
```
123+
124+
进行声明合并后,等同于:
125+
126+
```ts
127+
namespace Animals {
128+
export interface Legged {
129+
numberOfLegs: number;
130+
}
131+
export class Zebra {}
132+
export class Dog {}
133+
}
134+
```
135+
136+
这种模式的命名空间合并很好理解,但我们还需要了解非导出成员的情况。非导出成员只在原始的(未合并的)命名空间中可见,这意味着在合并之后,来自其它声明的合并成员无法访问非导出成员。
137+
138+
看下面的例子会更加直观:
139+
140+
```ts
141+
namespace Animal {
142+
let haveMuscles = true;
143+
export function animalsHaveMuscles() {
144+
return haveMuscles;
145+
}
146+
}
147+
namespace Animal {
148+
export function doAnimalsHaveMuscles() {
149+
return haveMuscles; // Error, because haveMuscles is not accessible here
150+
}
151+
}
152+
```
153+
154+
因为 `haveMuscles` 没有导出,所以只有共享相同的未合并命名空间的 `animalsHaveMuscles` 函数才能访问它。`doAnimalsHaveMuscles` 函数虽然是合并后的 `Animal` 命名空间的一部分,但它无法访问这个未导出的成员。
155+
156+
### 合并命名空间和类、函数、枚举
157+
158+
命名空间非常灵活,它也能和其它类型的声明合并。要做到这一点,命名空间声明必须跟在要合并的声明后面。最终的声明将具有两个声明类型的所有属性。TypeScript 使用这种能力对 JavaScript 和其它编程语言中的模式进行复刻。
159+
160+
### 合并命名空间和类
161+
162+
这为开发者提供了一种描述内部类的方式:
163+
164+
```ts
165+
class Album {
166+
label: Album.AlbumLabel;
167+
}
168+
namespace Album {
169+
export class AlbumLabel {}
170+
}
171+
```
172+
173+
合并成员的可见性规则和[合并命名空间这一小节](#合并命名空间)描述的一样,所以我们必须导出 `AlbumLabel` 这个类,以方便合并后的类去访问它。最终我们会得到一个类,其内部包含另一个类。你也可以使用命名空间向已有的类添加更多的静态成员。
174+
175+
除了内部类这种模式,你还可能熟悉使用 JavaScript 创建一个函数,之后通过添加属性对函数进行拓展。TypeSCript 借助声明合并,以一种类型安全的方式去构建这种模式。
176+
177+
```ts
178+
function buildLabel(name: string): string {
179+
return buildLabel.prefix + name + buildLabel.suffix;
180+
}
181+
namespace buildLabel {
182+
export let suffix = "";
183+
export let prefix = "Hello, ";
184+
}
185+
console.log(buildLabel("Sam Smith"));
186+
```
187+
188+
类似地,命名空间也可以用于为枚举拓展静态成员:
189+
190+
```ts
191+
enum Color {
192+
red = 1,
193+
green = 2,
194+
blue = 4,
195+
}
196+
namespace Color {
197+
export function mixColor(colorName: string) {
198+
if (colorName == "yellow") {
199+
return Color.red + Color.green;
200+
} else if (colorName == "white") {
201+
return Color.red + Color.green + Color.blue;
202+
} else if (colorName == "magenta") {
203+
return Color.red + Color.blue;
204+
} else if (colorName == "cyan") {
205+
return Color.green + Color.blue;
206+
}
207+
}
208+
}
209+
```
210+
211+
### 不允许的合并
212+
213+
在 TypeScript 中,不是所有的合并都是允许的。就目前而言,类无法和其它类或者变量合并。关于模拟类合并的信息,可以查阅[ TypeScript 中的混入](https://www.typescriptlang.org/docs/handbook/mixins.html)这一小节。
214+
215+
### 模块增强
216+
217+
虽然 JavaScript 的模块不支持合并,但是你可以通过导入并更新模块,来实现对已有对象的增强。我们来看一个简易的观察者示例:
218+
219+
```ts
220+
// observable.ts
221+
export class Observable<T> {
222+
// ......
223+
}
224+
// map.ts
225+
import { Observable } from "./observable";
226+
Observable.prototype.map = function (f) {
227+
// ......
228+
};
229+
```
230+
231+
在 TypeScript 中,这段代码也能运行,但是编译器并不了解 `Observable.prototype.map`。你可以使用模块增强告知编译器它的信息:
232+
233+
```ts
234+
// observable.ts
235+
export class Observable<T> {
236+
// ......
237+
}
238+
// map.ts
239+
import { Observable } from "./observable";
240+
declare module "./observable" {
241+
interface Observable<T> {
242+
map<U>(f: (x: T) => U): Observable<U>;
243+
}
244+
}
245+
Observable.prototype.map = function (f) {
246+
// ......
247+
};
248+
// consumer.ts
249+
import { Observable } from "./observable";
250+
import "./map";
251+
let o: Observable<number>;
252+
o.map((x) => x.toFixed());
253+
```
254+
255+
模块名的解析方式和 `import/export` 中的模块修饰符的解析方式相同。查阅[模块](https://www.typescriptlang.org/docs/handbook/modules.html)这一章以了解更多信息。模块增强中的声明会被合并,就好像它们是在原文件中声明的一样。
256+
257+
但是,这里有两个限制需要注意:
258+
259+
1. 你不能在模块增强中去创建一个顶级声明 —— 你只能增强已有的声明
260+
2. 默认导出无法被增强,只有命名导出才能被增强(因为你需要通过导出的名字对导出进行增强,而 `default` 是一个保留字 —— 查阅 [#14080](https://github.com/Microsoft/TypeScript/issues/14080) 了解更多细节)。
261+
262+
### 全局增强
263+
264+
你也可以从模块内部向全局作用域添加声明:
265+
266+
```ts
267+
// observable.ts
268+
export class Observable<T> {
269+
// ......
270+
}
271+
declare global {
272+
interface Array<T> {
273+
toObservable(): Observable<T>;
274+
}
275+
}
276+
Array.prototype.toObservable = function () {
277+
// ...
278+
};
279+
```
280+
281+
全局增强和模块增强具有一样的行为和限制。

0 commit comments

Comments
 (0)