学懂你也可以装逼-带你认识TypeScript的协变/逆变/双变

200 阅读2分钟

前言

TypeScript 基础类型的类型比较相信大家都有所了解,可以查看下图:

image.png

自上而下分别是:

顶级类型=>顶级原型=>包装类型=>基础类型=>对应的字面量类型=>底层类型

但是对于函数类型的类型层级是怎么样的呢? 这就是我们涉及到我们这篇文章所所说到的概念协变逆变

函数的类型比较

TS中对于函数类型的定义主要有两方面组成,参数返回值,所以我们对于函数的类型比较也必定围绕这两个来进行。

为了方便描述我这里简化一下描述,假设我们有三个类型Animal、Cat、Boer,它们的关系从命名也可以看出Animal => Cat => Boer,分别用这三个类型作为参数类型和返回值类型,很容易我们就能得到如下几个函数签名类型:

Animal -> Animal
Animal -> Cat
Animal -> Boer
Cat -> Animal
Cat -> Cat 
Cat -> Boer
Boer -> Animal
Boer -> Cat
Boer -> Boer

那么在这些函数类型之中到底谁会是Cat -> Cat的子类型呢?

中间推论的过程比较啰嗦且繁琐,我们就直接来个结论吧,在上面的例子中

Cat -> Cat 的子类型是Animal -> Boer

根据结果这个我们可以归纳出子函数类型子类的结论:

  1. 参数必须是当前比较类型的父类
  2. 返回值必须是当前比较类型的子类

协变与逆变

根据我们上面得到的结论 Animal -> Boer <= Cat -> Cat , 那么我们可以抽象一下得出两个结论

  • Boer <= Cat, T -> Boer <= T -> Cat
  • Animal >= Cat, Animal -> T <= Cat -> T

上面这两个结论就是协变逆变

双变

但是在实际上我们项目中一般是不会开启全部的协变与逆变的校验的。我们可以通过TSConfig 中的 StrictFunctionTypes去开启逆变检查

那么假如我们不开启这个逆变检查的话,结果会怎么样?

结合上面的例子,我们再举一个例子:

const func1: BoerFunc = fn; 
const func2: AnimalFunc = fn;

如果赋值成立,说明 fn 的类型是 BoerFunc / AnimalFunc 的子类型。 我们很自然得出以下的结果:

  1. (Cat -> T) ≼ (Boer -> T)
  2. (Cat -> T) ≼ (Animal -> T)

结合上面逆变的结论,第二点很明显是不对的。但是TS校验却不会报错,这个是因为当我们没开启StrictFunctionTypes的校验时,函数类型校验默认采用双变的校验方式。

双变(  bivariant   ,即逆变与协变都被认为是可接受的