TypeScript中的协变和逆变

Published on
Reading time
7 min read
Authors

关于这个主题,有很多概念需要了解,接下来通过一些问题理解关于父类型,子类型,以及集合,不可变性等的概念

概念

关于子类型,父类型的概念其实容易搞混,我用通俗的话去解释一下:在TypeScript的class里面,子class继承父class然后添加了一堆的方法属性等的场景下

typescriptEavan.dev
class Animal {
  name: string
}

class Cat extends Animal {
  breed: string
  age: string
}

这时候:

  1. 子类比父类拥有更多的属性

  2. 子类比父类更具体 (下面有一个例外情况)

然后子类型和父类型有一个很重要的概念: 父类型能被子类型 assign ,你可以理解为

  1. 父类型是一个熵很高,模糊的集合,这个集合里面的东西都是模棱两可ambiguous.(ps:人生来混沌,我们通过不断地学习,迭代,才能减小熵,让自己更完美)
  2. 然后子类型是基于父类型的,它通过更具象一些原本模糊的属性,通过规范拓展(extends,&)父类型,而不是或(|)等这种扩大熵增

因为在ts的类型系统中,我们一般都是要求类型越窄越精准越好,这是因为类型越窄,ts的语言系统才能帮我们更好得去检查出想要的问题,所以我们得出一个结论

子类型能assign赋予给父亲类型

题目1

解释一下以下为什么不会报错

typescriptEavan.dev
type Type1 = {}
type Type2 = {}

type NamedVariable = (Type1 | Type2) & { name: string }

const b: NamedVariable = {
  name: 'eavan',
}

其实Type1和Type2是一个空的对象,空对象的里面的key是不是随心所欲?

但是 { name: string } 和一个空对象类型取交集不依然是这个 { name: string }

题目2

tsxEavan.dev
type A = 3 | 4 | 5

type B = 3 | 4

请问谁是谁的子类型

  • 答案: B是A的子类型

为啥呢?看起来A不是比B多一个可选类型吗,为什么A是父类型,反而B是子类型?

因为这是一个联合类型,A目前有3个可能性,B是否只有2个可能性? 这样来说B是不是比A要更加具体呢?


通过以上学习,你应该就能理解什么是Covariance协变了

但是什么是逆变呢,应该就是反过来吧?那什么场景下是逆变呢

题目3

猜一猜D是什么类型

tsxEavan.dev
type D = number[] extends readonly number[] ? true : false

答案是true

这里面又涉及到一个不可变性和可变性的概念

类型安全:当一个函数接受一个 readonly number[] 作为参数时,它可以假设数组不会被修改。这保证了函数的行为是可预测的和安全的。

赋值规则:由于 readonly number[] 只是增加了一个不可变性约束,而没有改变数组元素的类型,所以从类型系统的角度看,number[] 赋值给 readonly number[] 是安全的。虽然 number[] 更加宽泛,但它符合 readonly number[] 的所有约束。

一文读懂 TS 中 Object, object, 类型之间的区别

逆变的场景

先说结果,逆变基本发生在函数的场景下

你可以通过以上几个小例子去分清谁能assign谁,但是以下几个类型你怎么assign?是不是很懵逼

tsxEavan.dev
type A12 = 1 | 2

type B123 = 1 | 2 | 3

type FnA12 = (p: A12) => void

type FnB123 = (p: B123) => void

type CCC = FnA12 extends FnB123 ? true : false

ts在线

猜一猜这时候CCC是true还是false

答案是false,然后有人会说:不是FnA12更具体吗?明明参数都只能选2个?

其实在函数里面,这套规则就不适用与之前我们所说的协变的场景了,并且这个可是函数的参数啊,维度又提升了一层,不是我们之前比谁在谁的集合等这种小游戏…

但是我们要怎么理解这种思维180°倒车的场景呢?

用逆向思维去解决这种问题

你要想哈,

  • FnB12的参数就1和2,是不是就只能处理传过来参数为1或者2的场景,
  • 但是FnB123就不一样了,他不管你是1还好2还好,甚至是3,都能三下五除二得搞定,
  • 那是不是用前朝的剑(FnB123) 就能斩本朝的官(当参数为1或者2时)?
  • 那是不是就是 FnB123 能 assign FnA12

在函数中,参数类型是逆变的,因为如果一个函数能接受更泛化的类型,那么它也可以接受更具体的类型。这意味着一个接受所有可能参数(如1, 2, 3)的函数,可以安全地替代一个只接受特定参数(如1或2)的函数。

总结

在TypeScript中,协变允许使用比原本预期的类型更加具体的类型。反之,逆变允许使用比原本预期的类型更加泛化的类型。

这下基本能理解什么是协变什么是逆变了把,当然ts上默认是双向协变的,你需要开一个 strictFunctionType 的选项才能享受到这种逆变的快感…