TypeScript Declaration Merging(声明合并)
TypeScript中有一些独特的概念,来自需要描述JavaScript对象类型发生了哪些变化。举个例子,最为独特的概念就是"声明合并"。理解了这个概念将会对你在当前JavaScript项目中使用TypeScript开发很有帮助。同时也打开了了解更高级抽象概念的门。
就本文目的而言,声明合并是指编译器执行将两个名称相同的声明合并到一个单独的声明里的工作。合并后的声明具有两种原始声明的特性。当然,声明合并不限于合并两个声明,需要合并的声明数量可任意(注意:他们之间具有相同名称)。
基本概念
在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 |
理解每一个声明创建的是什么,将有助于你理解当执行声明合并时对什么进行合并。
接口合并
最简单也是最常见的声明合并就是接口合并。在编译最底层,声明合并机制将两个已声明的成员加入到一个相同名称的接口中。
interface Box { height: number; width: number; } interface Box { scale: number; } var box: Box = {height: 5, width: 6, scale: 10};
接口中的非函数成员必须是唯一的,如果两个/多个接口同时声明相同名称的非函数成员,编译器就会扔出一个错误。
对于函数成员,相同名称的函数成员被视为这个函数的重载。值的注意的是,接口A和后面的接口A(这里成为A')合并,接口A‘中重载的函数将会比接口A中的同一个函数具有更高的优先级。
看案例:
interface Document { createElement(tagName: any): Element; } interface Document { createElement(tagName: string): HTMLElement; } interface Document { createElement(tagName: "div"): HTMLDivElement; createElement(tagName: "span"): HTMLSpanElement; createElement(tagName: "canvas"): HTMLCanvasElement; }
这三个接口将被合并到一个单独的声明。注意,每组接口内部的顺序依旧保持相同,只是每个组之间被合并,并且排在后面的接口的成员在新声明中被放到前面。
interface Document { createElement(tagName: "div"): HTMLDivElement; createElement(tagName: "span"): HTMLSpanElement; createElement(tagName: "canvas"): HTMLCanvasElement; createElement(tagName: string): HTMLElement; createElement(tagName: any): Element; }
模块合并
类似于接口,相同名称的模块也会对其成员进行合并。由于模块会创建一个命名空间和一个值,我们需要理解它们是如何合并的。
合并命名空间的时候,每个模块声明的输出接口的类型定义将进行合并,同名命名空间合并成一个单独的内部包含合并后接口的命名空间。
合并值的时候,如果已存在一个给定名称模块,那么后面的模块内的输出成员将被添加到这个模块。
看看这个例子中的Animal模块的声明合并:
module Animals {
export class Zebra { }
}
module Animals {
export interface Legged { numberOfLegs: number; }
export class Dog { }
}
相当于:
module Animals {
export interface Legged { numberOfLegs: number; }
export class Zebra { }
export class Dog { }
}
这个案例是学习模块合并很好的开始,但是想要更完整的理解,我们还需要理解非导出成员发生了什么。非导出成员只在原始(未合并)模块可见。这意味着,在合并之后,来自其他声明的合并后成员不能访问到非导出成员。
我们可以看个详细的解释:
module Animal { var haveMuscles = true; export function animalsHaveMuscles() { return haveMuscles; } } module Animal { export function doAnimalsHaveMuscles() { return haveMuscles; // <-- 错误, haveMuscles在这里不能访问 } }
因为haveMuscles这个成员未被输出,只有与其共享未合并的模块的animalsHaveMuscles函数能够访问这个symbol。尽管doAnimalsHaveMuscles函数是合并后的模块的一部分,但它还是不能访问到这个其他被合并的同名模块内的未输出成员。
模块与类、函数、枚举的合并
模块具有足够的灵活性,它可以与其他类型的声明进行合并。模块的声明必须遵循与其合并的声明。最终合并后的声明将包含两个声明类型的属性。Typescript使用这个功能去实现一些JavaScript里的模式。
第一个模块合并案例,我们将模块和类合并。这给用户提供了描述内部类的方法:
class Album { label: Album.AlbumLabel; } module Album { export class AlbumLabel{ name:string; show(){ console.log(this.name); } constructor(name:string){ this.name = name; } } } var newAlbum = new Album.AlbumLabel("Ys"); newAlbum.show();
合并后成员的可访问性规则和上一节的"模块合并"一样,所以我们必须export AlbumLabel类,为了让合并后的类可访问它。最终的结果是一个类的内部存在另一个类。你也可以使用模块现有的类添加更多的静态成员。
class Test{ fn:Test.TestFn } module Test { export var Value:string = "World"; export class TestFn{ show(name:string){ console.log(name+" "+Value); } } } var newTest = new Test.TestFn(); newTest.show("Hello");
除了内部类的模式,你可能对JavaScript中创建一个函数稍后再扩展其属性的做法已经很熟悉了。TypeScript使用声明合并达到这个目的,并且确保了类型安全。
function buildLabel(name: string): string { return buildLabel.prefix + name + buildLabel.suffix; } module buildLabel { export var suffix = ""; export var prefix = "Hello, "; } alert(buildLabel("Sam Smith"));
同样,模块也可以用来扩展枚举的静态成员:
enum Color { red = 1, green = 2, blue = 4 } module 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; } } } alert(Color.mixColor("yellow"));
不被允许的合并
在TypeScript中,并非所有的合并都被允许。目前为止,类不能与类合并,变量和类不能合并,接口和类也不能合并。需要模仿类的合并,请参考上一节:Typescript Mixins(混合)