类型的关系不外乎子母、无关、相等等这类。
一般的,如果 A 是 B 的子类型,则记作 A <= B 那么显然:
- 如果 A <= B 且 B <= A 那么 A == B
- 如果说 A != B, 则 A 不是 B 的子类型、B 也不是 A 的子类型; 反之亦然; 这类关系揭示了两类型无关的现象
TypeScript 采用了结构化类型,因此很自然地如此理解子类型:
// 有名字的 object
interface Animal { name: string };
// 会狗叫的 Animal
interface Dog extends Animal { bark(): void };
// 阿黄
interface YellowDog extends Dog { color: string };
// 会猫叫的 animal
interface Cat extends Animal { meow(): void };
// YellowDog <= Dog <= Animal
// Cat <= Animal一般来说, 类型变换指的是type A 到 B 的过程, 从这个层面来说, 任何一个泛型 GG<T> 就是一种类型变换 T -> GG<T> (或者叫做类型映射... 数学上很多名词是相通的 dddd);
如果 A <= B 那么 GG<A> <= GG<B> 还会成立吗 ?
命题: 如果
A <= B那么GG<A> <= GG<B>还会成立吗 ?
- 如果命题为真, 我们称 GG 是协变的
- 如果命题为假, 经过 GG 的变换后得到反过来的
GG<A> >= GG<B>则我们称 GG 是逆变的
协变与逆变描述了类型变换的性质,对于推导类型关系很重要。
一般来说协变更符合我们的直觉, 比如 Array<T> 是协变的, 我举几个逆变的例子, 比如数组排序的 sort Sort<T>
// Array.prototype.sort 排序子
type Sort<T> = (a: T, b: T) => number;为什么说 Sort 是逆变的呢? 你想下:
Dog <= Animal
论证 Sort<Dog> 和 Sort<Animal> 的关系
泛型实例化, 展开:
令 A' = Sort<Dog> = (a: Dog, b: Dog) => number
令 B' = Sort<Animal> = (a: Animal, b: Animal) => number
显然 A' 不能传 Animal, 因为 Animal 里可能有 Cat 不能作为 Dog;
而对于 B' 调用的时候可传 Animal, 也可传 Dog 也 ok, 因为 Dog 也是 Animal, 换言之, A' 能的它也能, B' 能的 A' 不能, 所以 B' 是 A' 的子类型;
也就是 Sort<Animal> <= Sort<Dog>
从这个例子也可以看出, 一般牵扯到 fn 的类型变换会导致逆变;如果没有函数的参与,一般情况下变换都是协变的。
如果不开启严格模式, 上面那个 Sort 是不严格区分协变逆变的, 也就是所谓的双向协变 bivariant Sort<Animal> <=> Sort<Dog>;
这个可以参考这方面的说明 https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-6.html
为了类型安全, 你更应该开启严格模式.
还有种情况也可以称呼为双向协变,很少见:
type FF<X, Y> = (a: X) => Y;若 A <= B, 则 FF<B, 1> <= FF<A, 1> 或 FF<1, A> <= FF<1, B>
此时 FF 是双向协变的,讨论关系变化的时候得考虑类似偏导数的方式固定一个泛型来讨论。
数组很显然是协变的, 但如果数组 Array<T> 设计成逆变的... 那么:
let animals: Animal[] = [cat, dog];
let dogs: Dog[] = [dog];
// Case 1: 逆变数组
dogs = animals; // 如果数组逆变, 这里不会报错; 如果数组协变, 这里会报错;
dogs[0].bark(); // 运行时报错, dogs[0] 其实是 animals[0] 而它是 cat ...
// 所以逆变数组用起来很危险的显然, 逆变数组很不安全, 还好 TypeScript 在设计的时候数组是协变的, 但是这真的万无一失吗? 来看看这个 case 下的逆变:
let animals: Animal[] = [cat, dog];
let dogs: Dog[] = [dog];
// Case 2: 协变数组也不安全
animals = dogs; // animals 有 dogs 的引用
animals.push(cat); // 往 animals push cata .... 本质上是给 dogs push cat
dogs.forEach(d => d.bark()); // 里面混有一只 cat 这里会 runtime error
// 但静态类型检查没有检查到这个问题所以... 协变数组不一定安全, 最安全是数组是不可变协变数组 readonly T[];
大部分语言都没有给出这类 case 的完美解决方案,TS 也一样,Safe Mutable Array 只有少数语言解决了,比如 Rust