类型兼容性

    如 类型与 number 类型不兼容:

    TypeScript 类型系统设计比较方便,它允许你有一些不正确的行为。例如:任何类型都能被赋值给 any,这意味着告诉编译器你可以做任何你想做的事情:

    1. const foo: any = 123;
    2. foo = 'hello';
    3. foo.toPrecision(3);

    结构化

    TypeScript 对象是一种结构类型,这意味着只要结构匹配,名称也就无关紧要了:

    1. interface Point {
    2. x: number;
    3. y: number;
    4. }
    5. class Point2D {
    6. constructor(public x: number, public y: number) {}
    7. }
    8. let p: Point;
    9. // ok, 因为是结构化的类型
    10. p = new Point2D(1, 2);

    这允许你动态创建对象(就好像你在 vanilla JS 中使用一样),并且它如果能被推断,该对象仍然具有安全性。

    1. interface Point2D {
    2. x: number;
    3. y: number;
    4. }
    5. interface Point3D {
    6. x: number;
    7. y: number;
    8. z: number;
    9. }
    10. const point2D: Point2D = {
    11. x: 0,
    12. y: 0
    13. };
    14. const point2D: Point2D = { x: 0, y: 10 };
    15. const point3D: Point3D = { x: 0, y: 10, z: 20 };
    16. function iTakePoint2D(point: Point2D) {
    17. /* do something */
    18. }
    19. iTakePoint2D(point2D); // ok, 完全匹配
    20. iTakePoint2D(point3D); // 额外的信息,没关系
    21. iTakePoint2D({ x: 0 }); // Error: 没有 'y'

    对类型兼容性来说,变体是一个利于理解和重要的概念。

    对一个简单类型 BaseChild 来说,如果 ChildBase 的子类,Child 的实例能被赋值给 Base 类型的变量。

    TIP

    这是多态性。

    • 协变(Covariant):只在同一个方向;
    • 逆变(Contravariant):只在相反的方向;
    • 双向协变(Bivariant):包括同一个方向和不同方向;
    • 不变(Invariant):如果类型不完全相同,则它们是不兼容的。

    TIP

    对于存在完全可变数据的健全的类型系统(如 JavaScript),Invariant 是一个唯一的有效可选属性,但是如我们说讨论的,便利性迫使我们作出一些不是很安全的选择。

    关于协变和逆变的更多内容,请参考:。

    函数

    当你在比较两个函数时,这有一些你需要考虑到的事情。

    协变(Covariant):返回类型必须包含足够的数据。

    1. interface Point2D {
    2. x: number;
    3. y: number;
    4. }
    5. interface Point3D {
    6. x: number;
    7. y: number;
    8. z: number;
    9. }
    10. let iMakePoint2D = (): Point2D => ({ x: 0, y: 0 });
    11. let iMakePoint3D = (): Point3D => ({ x: 0, y: 0, z: 0 });
    12. iMakePoint2D = iMakePoint3D;
    13. iMakePoint3D = iMakePoint2D; // ERROR: Point2D 不能赋值给 Point3D

    更少的参数数量是好的(如:函数能够选择性的忽略一些多余的参数),但是你得保证有足够的参数被使用了:

    1. const iTakeSomethingAndPassItAnErr = (x: (err: Error, data: any) => void) => {
    2. /* 做一些其他的 */
    3. };
    4. iTakeSomethingAndPassItAnErr(() => null); // ok
    5. iTakeSomethingAndPassItAnErr(err => null); // ok
    6. iTakeSomethingAndPassItAnErr((err, data) => null); // ok
    7. // Error: 参数类型 `(err: any, data: any, more: any) => null` 不能赋值给参数类型 `(err: Error, data: any) => void`
    8. iTakeSomethingAndPassItAnErr((err, data, more) => null);

    可选的(预先确定的)和 Rest 参数(任何数量的参数)都是兼容的:

    Note

    双向协变(Bivariant):旨在支持常见的事件处理方案。

    1. // 事件等级
    2. interface Event {
    3. timestamp: number;
    4. }
    5. interface MouseEvent extends Event {
    6. x: number;
    7. y: number;
    8. }
    9. interface KeyEvent extends Event {
    10. keyCode: number;
    11. }
    12. // 简单的事件监听
    13. enum EventType {
    14. Mouse,
    15. Keyboard
    16. }
    17. function addEventListener(eventType: EventType, handler: (n: Event) => void) {
    18. // ...
    19. }
    20. // 不安全,但是有用,常见。函数参数的比较是双向协变。
    21. addEventListener(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));
    22. // 在安全情景下的一种不好方案
    23. addEventListener(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
    24. addEventListener(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));
    25. // 仍然不允许明确的错误,对完全不兼容的类型会强制检查
    26. addEventListener(EventType.Mouse, (e: number) => console.log(e));

    同样的,你也可以把 Array<Child> 赋值给 Array<Base> (协变),因为函数是兼容的。数组的协变需要所有的函数 Array<Child> 都能赋值给 Array<Base>,例如 push(t: Child) 能被赋值给 push(t: Base),这都可以通过函数参数双向协变实现。

    这对于来自其他语言的人来说,可能会感到很困惑,但他们希望以下错误不会出现在 TypeScript 中:

    1. interface Poin2D {
    2. x: number;
    3. y: number;
    4. }
    5. let iTakePoint2D = (point: Point2D) => {};
    6. let iTakePoint3D = (point: Point3D) => {};
    7. iTakePoint3D = iTakePoint2D; // ok, 这是合理的
    8. iTakePoint2D = iTakePoint3D; // ok,为什么?
    • 枚举与数字类型相互兼容
    1. enum Status {
    2. Ready,
    3. Waiting
    4. }
    5. let status = Status.Ready;
    6. let num = 0;
    7. status = num;
    8. num = status;
    • 来自于不同枚举的枚举变量,被认为是不兼容的:
    1. enum Status {
    2. Ready,
    3. Waiting
    4. }
    5. enum Color {
    6. Red,
    7. Blue,
    8. Green
    9. }
    10. let status = Status.Ready;
    11. let color = Color.Red;
    12. status = color; // Error

    • 仅仅只有实例成员和方法会相比较,构造函数和静态成员不会被检查。
    1. class Animal {
    2. feet: number;
    3. constructor(name: string, numFeet: number) {}
    4. }
    5. class Size {
    6. feet: number;
    7. constructor(meters: number) {}
    8. }
    9. let a: Animal;
    10. a = s; // OK
    11. s = a; // OK
    • 私有的和受保护的成员必须来自于相同的类。

    TypeScript 类型系统基于变量的结构,仅当类型参数在被一个成员使用时,才会影响兼容性。如下例子中,T 对兼容性没有影响:

    1. interface Empty<T> {}
    2. let x: Empty<number>;
    3. let y: Empty<string>;
    4. x = y; // ok

    T 被成员使用时,它将在实例化泛型后影响兼容性:

    1. interface Empty<T> {
    2. data: T;
    3. }
    4. let x: Empty<number>;
    5. let y: Empty<string>;
    6. x = y; // Error

    如果尚未实例化泛型参数,则在检查兼容性之前将其替换为 any

    1. let identity = function<T>(x: T): T {
    2. // ...
    3. };
    4. let reverse = function<U>(y: U): U {
    5. // ...
    6. };
    7. identity = reverse; // ok, 因为 `(x: any) => any` 匹配 `(y: any) => any`

    类中的泛型兼容性与前文所提及一致:

    1. class List<T> {
    2. add(val: T) {}
    3. }
    4. class Animal {
    5. name: string;
    6. }
    7. class Cat extends Animal {
    8. meow() {
    9. // ..
    10. }
    11. }
    12. const animals = new List<Animal>();
    13. animals.add(new Animal()); // ok
    14. animals.add(new Cat()); // ok
    15. const cats = new List<Cat>();
    16. cats.add(new Animal()); // Error
    17. cats.add(new Cat()); // ok

    脚注:不变性(Invariance)

    我们说过,不变性可能是唯一一个听起来合理的选项,这里有一个关于 contraco 的变体,被认为对数组是不安全的。

    1. class Animal {
    2. constructor(public name: string) {}
    3. }
    4. class Cat extends Animal {
    5. meow() {
    6. console.log('cat');
    7. }
    8. }
    9. let animal = new Animal('animal');
    10. let cat = new Cat('cat');
    11. // 多态
    12. // Animal <= Cat
    13. animal = cat; // ok
    14. cat = animal; // ERROR: cat 继承于 animal
    15. // 演示每个数组形式
    16. let animalArr: Animal[] = [animal];
    17. let catArr: Cat[] = [cat];
    18. // 明显的坏处,逆变
    19. // Animal <= Cat
    20. // Animal[] >= Cat[]
    21. catArr = animalArr; // ok, 如有有逆变
    22. catArr[0].meow(); // 允许,但是会在运行时报错
    23. // 另外一个坏处,协变
    24. // Animal <= Cat
    25. // Animal[] <= Cat[]
    26. animalArr = catArr; // ok,协变
    27. catArr.forEach(c => c.meow()); // 允许,但是会在运行时报错。