【TypeScript】类型兼容性&声明合并

2021/09/09 09:36:25

类型兼容性

对象类型比较

x 要兼容 y(可以把 y 赋值给 x),那么 y 至少具有与 x 相同的属性,即 x 的类型是 y 类型的子集。

interface Named {
  name: string;
}

let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: "Alice", location: "Seattle" };
x = y; // success

函数类型比较

参数列表

x 要兼容 y(可以把 y 赋值给 x),需要 x 的每个位置的参数必须能在 y 中的相应位置找到相同类型的参数。

下例中 x = y 赋值错误是因为 y 中有两个必填参数,而 x 中只有一个。

let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

x = y; // Error
y = x; // OK

返回值

x 要兼容 y(可以把 y 赋值给 x),需要 x 的返回值是 y 返回值类型的子集(子类型)。

let x = () => ({ name: "Alice" });
let y = () => ({ name: "Alice", location: "Seattle" });

x = y; // OK
y = x; // Error, because x() lacks a location property

class 的类型比较

比较两个类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。

如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。

声明合并

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

声明创建的实体

TypeScript 中的声明会创建以下三种实体之一:

  • 命名空间:创建命名空间的声明会新建一个命名空间,它包含了用 . 符号来访问时使用的名字。

  • 类型:用声明的模型创建一个类型并绑定到给定的名字上。

  • 值:创建在 JavaScript 输出中看到的值。

Declaration TypeNamespaceTypeValue
NamespaceYY
ClassYY
EnumYY
InterfaceY
Type AliasY
FunctionY
VariableY

接口合并

接口合并的机制是把双方的成员放到一个同名的接口里。

接口的非函数的成员应该是唯一的。如果它们不是唯一的,那么它们必须是相同的类型。

对于函数成员,每个同名函数声明都会被当成这个函数的一个重载。 后面的接口具有更高的优先级(其重载会在靠前位置)。

如果签名里有一个参数的类型是 单一的字符串字面量(比如,不是字符串字面量的联合类型),那么它将会被提升到重载列表的最顶端(依然遵循后来者居上)。

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;
}

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

interface Document {
  createElement(tagName: "canvas"): HTMLCanvasElement;
  createElement(tagName: "div"): HTMLDivElement;
  createElement(tagName: "span"): HTMLSpanElement;
  createElement(tagName: string): HTMLElement;
  createElement(tagName: any): Element;
}

命名空间合并

namespace 关键字会创建出命名空间和值,命名空间的合并也是这两者的合并。

导出成员合并

对于命名空间的合并:模块导出的同名接口进行合并,参考上面的接口合并规则。

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

namespace Animals {
    export class Zebra { }
}

namespace Animals {
    export interface Legged { numberOfLegs: number; }
    export class Dog { }
}

等同于:

namespace Animals {
    export interface Legged { numberOfLegs: number; }

    export class Zebra { }
    export class Dog { }
}

非导出成员合并

非导出成员仅在其原有的(合并前的)命名空间内可见。这就是说合并之后,从其它命名空间合并进来的成员无法访问非导出成员。

namespace Animal {
    let haveMuscles = true;

    export function animalsHaveMuscles() {
        return haveMuscles;
    }
}

namespace Animal {
    export function doAnimalsHaveMuscles() {
        return haveMuscles;  // Error, because haveMuscles is not accessible here
    }
}

因为 haveMuscles 并没有导出,只有 animalsHaveMuscles 函数共享了原始未合并的命名空间可以访问这个变量。 doAnimalsHaveMuscles 函数虽是合并命名空间的一部分,但是访问不了未导出的成员。

命名空间与其他类型合并

命名空间与其他类型合并就是对其他类型的扩展。

命名空间与类合并(内部类)

合并结果是一个类并带有一个内部类。

必须在命名空间中导出 AlbumLabel 类,合并的类才能访问。

class Album {
    label: Album.AlbumLabel;
}
namespace Album {
    export class AlbumLabel { }
}

命名空间与函数合并(扩展函数)

function buildLabel(name: string): string {
    return buildLabel.prefix + name + buildLabel.suffix;
}

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

buildLabel("Sam Smith");

命名空间与枚举合并(扩展枚举)

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;
        }
    }
}
Color.mixColor('yellow');

类合并(混入)

类的合并可以使用 mixins混入 模式来实现。

implements 关键字让一个类实现其他类从而实现混入。

把类当成了接口,仅被实现类的类型而非其实现。

class Disposable {
  isDisposed: boolean;
}

// Activatable Mixin
class Activatable {
  isActive: boolean = true;
}

class SmartObject implements Disposable, Activatable {
  isActive;
  isDisposed;
}