协变与逆变

协变与逆变的推断

TS 中的协变与逆变是针对函数标签类型的。用于来比较函数标签的类型层级。

我们有如下两个函数标签

class Animal {
	eating() {}
}

class Dog extends Animal {
  bark() {}
}

type AnimalFactory = (arg: Animal) => Animal;
type DogFactory = (arg: Dog) => Dog;

接下来我将用 A < B 表示 A 为 B 的子类型。

比较上面两个函数类型标签,根据单个类型层级来讲, Dog < Animal 是成立的。那么,我们能否通过这个关系,推断出 DogFactory < AnimalFactory 呢 ?

我们从这样一个角度出发,假设我现在如下代码:

function test(dog: Dog) {
  dog.bark();
  return dog;
}

const myFun: AnimalFactory = test; // test 相当于 (dog: Dog) => Dog;

我们假设这个赋值语句是成立的,那么我们可以调用 myFun 这个函数, 并且传入一个 Animal 的实例对象。那么会出现什么问题呢 ? 这里我们的 test 函数内部逻辑实现,以来了 Dog 实例对象的方法,我们传入一个 Animal 类型的实例,那么久会导致代码执行错误。所以,DogFactory < AnimalFactory 是不成立的。

那么,我们反过来判断 AnimalFactory < DogFactory 是否成立

function test(animal: Animal) {
  animal.eating();
  return animal;
}

const myFun: DogFactory = test; // tes 相当于 (animal: Animal) => Animal;

同样,我们对于参数列表用上面的方式来判断,当我们调用 myFun 时,我们会传入 Dog 类型的对象,而 test 函数的逻辑依赖的 eating 方法,也是存在Dog实例上的,所以这个方法执行并不会报错。

根据上边判断看起来AnimalFactory < DogFactory 是成立的,但是我们上面的比较并没有考虑返回值类型。

接下来我们来考虑返回值类型的

function test(animal: Animal) {
  animal.eating();
  return animal;
}

const myFun: DogFactory = test; // tes 相当于 (animal: Animal) => Animal;
const dog: Dog = myFun(new Animal());

根据 myFun 的函数标签,我们可以认为它执行结束后,返回的值是 Dog类型的对象。那么,在这个假设的前提下,我们后面的逻辑必然存在调用Dog实例独有的属性或方法。但实际上,test 返回的是 Animal 对象的实例,那么它就会导致我们依赖该方法的返回值的代码,出现逻辑错误。所以 AnimalFactory < DogFactory 目前是不成立的。

我们接下来,把上面两个函数类型的返回值类型进行对调。

type DogFactory = (dog: Dog) => Animal;
type AnimalFactory = (animal: Animal) => Dog;
// 上面把名字反过来更好,但是还是先根据之前的描述和分析来写。
type AnimalFactory = (dog: Dog) => Animal;
type DogFactory = (animal: Animal) => Dog;

这个时候,我们在按照上面的比较逻辑进行比较,就会发现依赖返回值的代码并不会出现报错的情况,因为此时 myFun的返回值是Animal 那么后面,就算吧 myFun 赋值给返回 Dog 类型对象的函数,依然不会影响后面代码的逻辑。

到这里,我们就能够得出一个结论了 (animal: Animal) => Dog < (dog: Dog) => Animal 是成立的,也就是说 前者是后者的子类。

结论:如果函数标签 A 要满足是函数标签 B 的子类的话

  • 必须满足 A 的参数列表 是 B 的参数列表的父级(类型层级更高), 这就被称为 函数参数的 逆变
  • 必须满足 A 的返回值 是 B 的返回值的子类(类型层级更低),这就被称为 函数参数的 协变

测试代码:

class Animal {
    eating() {}
}

class Dog extends Animal {
    bark() {

    }
}

type res = ((animal: Animal) => Dog) extends ((dog: Dog) => Animal)  ? 1 : 2; // 1

正常情况下,函数参数默认为双变,即既可以协变又可以逆变。我们需要在 tsconfig 中开启函数严格检查模式strictFunctionTypes: true

类型编程练习

函数参数逆变 配合 extends、infer 的将联合类型转化成交叉类型

对于函数参数的逆变位置,通过 infer 推断,如果这个 infer 推断的位置有多个选项,那么这些选项就会被设置为交叉类型。

用把联合类型转换成交叉类型为例子:

type UnionType = "acwink" | "LL" |  "CC";
// 目标是将上面类型转换成 "acwink" & "LL" &  "CC"
type UnionToIntersection<T> = (T extends unknown ? (arg: T) => unknown: never) extends (arg: infer U) => unknown ? U : never;
type Res = UnionToIntersection<UnionType>; // "acwink" & "LL" & "CC"

函数参数逆变 配合 extends、infer 获取联合类型最后一个类型

这里我们必须要先知道一个概念,对于函数的交叉类型 使用 extends () => infer R,Typescript 会取当前函数交叉类型的最后一个类型去匹配赋值。

// 目标: "jakc" | "alice" | "acwink" 取这个联合类型的最后一个值。
// 我们知道了上面对于函数交叉类型的特殊性质,我们就可以想办法把联合类型转换成函数类型的交叉形式,那么这个过程当然要使用我们的函数参数的逆变在infer推断中的特性啦
type UnionToIntersectionFn<T> = (T extends unknown ? (arg: () => T) => unknown : never) extends (arg: infer R) => unknown ? R : never;
type GetUnionLast<T> = UnionToIntersectionFn<T> extends () => infer R ? R : never;
type Res = GetUnionLast<"jakc" | "alice" | "acwink">; // "acwink"

这些就是逆变的一些应用啦。