Skip to content

Instantly share code, notes, and snippets.

@eczn
Last active May 9, 2022 08:38
Show Gist options
  • Select an option

  • Save eczn/dcb1bc2aa6be5475e155fc75f51d71d1 to your computer and use it in GitHub Desktop.

Select an option

Save eczn/dcb1bc2aa6be5475e155fc75f51d71d1 to your computer and use it in GitHub Desktop.
莲学派 TypeScript 类型变换中的关系变化

类型关系

类型的关系不外乎子母、无关、相等等这类。 一般的,如果 A 是 B 的子类型,则记作 A <= B 那么显然:

  1. 如果 A <= B 且 B <= A 那么 A == B
  2. 如果说 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> 还会成立吗 ?

  1. 如果命题为真, 我们称 GG 是协变的
  2. 如果命题为假, 经过 GG 的变换后得到反过来的 GG<A> >= GG<B> 则我们称 GG 是逆变的

协变与逆变描述了类型变换的性质,对于推导类型关系很重要。

TypeScript 中的协变逆变举例

逆变的 Sort

一般来说协变更符合我们的直觉, 比如 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 的类型变换会导致逆变;如果没有函数的参与,一般情况下变换都是协变的。

双向协变 bivariant

如果不开启严格模式, 上面那个 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment