声明合并

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

    基础概念

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

    理解每个声明创建了什么,有助于理解当声明合并时有哪些东西被合并了。

    最简单也最常见的声明合并类型是接口合并。 从根本上说,合并的机制是把双方的成员放到一个同名的接口里。

    接口的非函数的成员应该是唯一的。 如果它们不是唯一的,那么它们必须是相同的类型。 如果两个接口中同时声明了同名的非函数成员且它们的类型不同,则编译器会报错。

    对于函数成员,每个同名函数声明都会被当成这个函数的一个重载。 同时需要注意,当接口与后来的接口A合并时,后面的接口具有更高的优先级。

    如下例所示:

    1. interface Cloner {
    2. clone(animal: Animal): Animal;
    3. }
    4. interface Cloner {
    5. clone(animal: Sheep): Sheep;
    6. }
    7. interface Cloner {
    8. clone(animal: Dog): Dog;
    9. clone(animal: Cat): Cat;
    10. }

    这三个接口合并成一个声明:

    1. interface Cloner {
    2. clone(animal: Dog): Dog;
    3. clone(animal: Cat): Cat;
    4. clone(animal: Sheep): Sheep;
    5. clone(animal: Animal): Animal;
    6. }

    注意每组接口里的声明顺序保持不变,但各组接口之间的顺序是后来的接口重载出现在靠前位置。

    比如,下面的接口会合并到一起:

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

    合并后的Document将会像下面这样:

    合并命名空间

    与接口相似,同名的命名空间也会合并其成员。 命名空间会创建出命名空间和值,我们需要知道这两者都是怎么合并的。

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

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

    Animals声明合并示例:

    1. namespace Animals {
    2. export class Zebra { }
    3. namespace Animals {
    4. export interface Legged { numberOfLegs: number; }
    5. export class Dog { }
    6. }

    等同于:

    1. export interface Legged { numberOfLegs: number; }
    2. export class Zebra { }
    3. export class Dog { }
    4. }

    除了这些合并外,你还需要了解非导出成员是如何处理的。 非导出成员仅在其原有的(合并前的)命名空间内可见。这就是说合并之后,从其它命名空间合并进来的成员无法访问非导出成员。

    下例提供了更清晰的说明:

    1. namespace Animal {
    2. let haveMuscles = true;
    3. export function animalsHaveMuscles() {
    4. return haveMuscles;
    5. }
    6. }
    7. namespace Animal {
    8. export function doAnimalsHaveMuscles() {
    9. return haveMuscles; // Error, because haveMuscles is not accessible here
    10. }
    11. }

    命名空间可以与其它类型的声明进行合并。 只要命名空间的定义符合将要合并类型的定义。合并结果包含两者的声明类型。 TypeScript使用这个功能去实现一些JavaScript里的设计模式。

    这让我们可以表示内部类。

    合并规则与上面合并命名空间小节里讲的规则一致,我们必须导出AlbumLabel类,好让合并的类能访问。 合并结果是一个类并带有一个内部类。 你也可以使用命名空间为类增加一些静态属性。

    除了内部类的模式,你在JavaScript里,创建一个函数稍后扩展它增加一些属性也是很常见的。 TypeScript使用声明合并来达到这个目的并保证类型安全。

    1. function buildLabel(name: string): string {
    2. return buildLabel.prefix + name + buildLabel.suffix;
    3. }
    4. namespace buildLabel {
    5. export let suffix = "";
    6. export let prefix = "Hello, ";
    7. }
    8. console.log(buildLabel("Sam Smith"));

    相似的,命名空间可以用来扩展枚举型:

    1. enum Color {
    2. red = 1,
    3. green = 2,
    4. blue = 4
    5. }
    6. if (colorName == "yellow") {
    7. return Color.red + Color.green;
    8. }
    9. else if (colorName == "white") {
    10. return Color.red + Color.green + Color.blue;
    11. }
    12. else if (colorName == "magenta") {
    13. return Color.red + Color.blue;
    14. }
    15. else if (colorName == "cyan") {
    16. return Color.green + Color.blue;
    17. }
    18. }
    19. }

    非法的合并

    TypeScript并非允许所有的合并。 目前,类不能与其它类或变量合并。 想要了解如何模仿类的合并,请参考。

    虽然JavaScript不支持合并,但你可以为导入的对象打补丁以更新它们。让我们考察一下这个玩具性的示例:

    1. // observable.ts
    2. export class Observable<T> {
    3. // ... implementation left as an exercise for the reader ...
    4. }
    5. // map.ts
    6. import { Observable } from "./observable";
    7. Observable.prototype.map = function (f) {
    8. // ... another exercise for the reader
    9. }

    它也可以很好地工作在TypeScript中, 但编译器对 Observable.prototype.map一无所知。 你可以使用扩展模块来将它告诉编译器:

    模块名的解析和用import/export解析模块标识符的方式是一致的。 更多信息请参考 Modules。 当这些声明在扩展中合并时,就如同在原始位置被声明一样。 但是,有两点限制需要注意:

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

    全局扩展

    1. // observable.ts
    2. export class Observable<T> {
    3. // ... still no implementation ...
    4. }
    5. declare global {
    6. interface Array<T> {
    7. toObservable(): Observable<T>;
    8. }
    9. }
    10. Array.prototype.toObservable = function () {

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