协变与逆变
非常陌生和高冷的两个词,深入 ts 的过程中遇到的词,究竟是什么意思呢
理解起来非常像是在学数学
学完之后能干嘛?
参考:
Intro
以下内容摘自维基百科
例如,如果Cat是Animal的子类型,那么Cat类型的表达式可用于任何出现Animal类型表达式的地方。所谓的变型(variance)是指如何根据组成类型之间的子类型关系,来确定更复杂的类型之间的子类型关系。
- 
IEnumerable<Cat>是IEnumerable<Animal>的子类型,因为类型构造器IEnumerable<T>是协变的(covariant)。注意到复杂类型IEnumerable的子类型关系和其接口中的参数类型是一致的,亦即,参数类型之间的子类型关系被保持住了。 - 
Action<Animal>是Action<Cat>的子类型,因为类型构造器Action<T>是逆变的(contravariant)。(在此,Action<T>被用来表示一个参数类型为T或sub-T的一级函数 (opens in a new tab))。注意到**T的子类型关系在复杂类型Action的封装下是反转的**,但是当它被视为函数的参数时其子类型关系是被保持的。 - 
IList<Cat>或IList<Animal>彼此之间没有子类型关系。因为IList<T>类型构造器是不变的(invariant),所以参数类型之间的子类型关系被忽略了。 
子类型
首先什么是子类型,形如 A extends B,A 的类型值能够赋值给 B 类型,通常对于基础类型可以认为是子集关系
declare let a: "name";
declare let b: string;
 
b = a; // ok a是b的子类型
a = b; // wrong对于对象类型,尝试将对应类型的变量赋值给另一个类型的变量
type A = {
  name: string;
  age: number;
};
type B = A & {
  id: number;
};
 
declare let a: A;
declare let b: B;
 
a = b; // ok
b = a; // wrong
b.id.toFixed(); // 不存在id字段所以 B 是 A 的子类型,B 比 A 更加具体(属性更多),B extends A 成立
- 在类型系统中,属性更多的类型是子类型。
 - 在集合论中,属性更少的集合是子集。
 
也就是说,子类型是父类型的超集,而父类型是子类型的子集,这是直觉上容易搞混的一点。
记住一个特征,子类型比父类型更加具体,这点很关键。
协变(covariant)
type A = {
  name: string;
  age: number;
};
type B = A & {
  id: number;
};
 
declare let a: A;
declare let b: B;
 
type Test<T> = {
  value: T;
};
 
declare let c: Test<A>;
declare let d: Test<B>;
 
d = c; // wrong
d.value.id.toFixed(); // 不存在id字段
 
c = d; // ok
c.value.name.toString();可以看出:A 是 B 的子类型,泛型类型 Test<T> 生成的 c,d 赋值的关系可以知道的是:Test<A> 是 Test<B> 的子类型
B是A的子类型,而Test<B>又是Test<A>的子类型,所以我们称范型Test<T>的类型参数T在value: T这个位置是协变的。
所谓协变,也就是子类型关系变化一致?
再看个例子
type Fn<T> = () => T;
declare let c: Fn<A>;
declare let d: Fn<B>;结论是:Fn<T> 的参数 T 在函数返回值这个位置是协变的。
逆变(contravariant)
先说结论,范型的类型参数在函数的参数位置上的逆变的。给定范型Fn<T> = (arg: T) => void,如果B是A的子类型,则Fn<B>是Fn<A>的父类型。
着实不懂,但可以通过最开始的赋值关系来判断
type A = {
  name: string;
  age: number;
};
type B = A & {
  id: number;
};
 
declare let a: A;
declare let b: B;
 
type Fn<T> = (arg: T) => void;
let c: Fn<A> = (arg: A) => console.log(arg.name.toString());
let d: Fn<B> = (arg: B) => console.log(arg.id.toFixed());
 
c = d; // wrong
// Type 'Fn<B>' is not assignable to type 'Fn<A>'.
//   Type 'A' is not assignable to type 'B'.
//     Property 'id' is missing in type 'A' but required in type '{ id: number; }'.
c(a); // wrong 运行时函数内部访问arg.id报错
 
d = c;
d(b); // ok 运行时函数内部访问arg.name和arg.age都是安全的可以看到当泛型在函数参数的位置,类型对应变量进行赋值,必须要 B 是 A 的子类型(B extends A)?
可以看这个例子
interface Animal {
  age: number
}
 
interface Dog extends Animal {
  bark(): void
}
 
let visitAnimal = (animal: Animal) => void;
let visitDog = (dog: Dog) => void;
// 其中 Dog extends Animalanimal = dog 是类型安全的(因为 dog 是 animal 的子类型,animal 所用到的属性 dog 必然都有,所以安全),那么 visitAnimal = visitDog 好像也是可行的?其实不然
比如实现如下
let visitAnimal = (animal: Animal) => {
  animal.age;
};
 
let visitDog = (dog: Dog) => {
  dog.age;
  dog.bark();
};在 visitDog 中会有 Animal 实例上没有的属性
不过 visitDog = visitAnimal 是安全可行的,因为 visitAnimal 内部所用到的属性 visitDog 内部必然也会有
所以 type MakeFunction<T> = (arg: T) => void 父子类型关系逆转了,这就是 逆变(Contravariance)
在 TS 中
当然,在 TypeScript 中,由于灵活性等权衡,对于函数参数默认的处理是 双向协变 的。也就是既可以 visitAnimal = visitDog,也可以 visitDog = visitAnimal。在开启了 tsconfig 中的 strictFunctionType 后才会严格按照 逆变 来约束赋值关系。
来点例子
type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;
type A = Foo<{ a: string; b: string }>; // string
type B = Foo<{ a: string; b: number }>; // string | number对于上方的范型Foo<T>,观察可知类型参数U所在的两个位置都是协变的,并且T是{ a: infer U, b: infer U}的子类型。
所以通过先验知识知道了 Foo<T> 的 U 是协变的(上一句),那就可以推导出:
type A的最终是 stringtype B的最终是 string | number,因为 string 既要是 U 的子类型,number 也得是 U 的子类型,所以U => string | number是联合类型
速记:同一个类型参数在协变位置上的多个候选将会推导成联合类型
再来看个
type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
  ? U
  : never;
type A = Bar<{ a: (x: string) => void; b: (x: string) => void }>; // string
type B = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // string & number首先观察得出:Bar<T>,观察可知类型参数U所在的两个位置都是逆变的。
所以:
- 对于 
type A,是 string 很好看出 - 对于 
type B,因为是逆变,所以U是 a string 的子类型,也得是 b number 的子类型,所以最大边界设定为string & number?(去 playground (opens in a new tab) 试了下 TS 5.2 推出来是never,我裂开) 
速记:同一个类型参数在逆变位置上的多个候选将会推导成交叉类型??
最后,至于为什么“返回值类型是协变的,而参数类型是逆变的”
也就是为什么
let returnAnimal = (): Animal => {
  return animal;
};
 
let returnDog = (): Dog => {
  return dog;
};
// Dog extends Animal
returnAnimal = returnDog; // 合法
 
let paramAnimal = (a: animal) => {};
let paramDog = (d: Dog) => {};
paramAnimal = paramDog; // 不合法!是考虑安全性的 block 不同:
- 返回值类型时,需要在掉用那个函数的 block 里考虑,相当于自身的类型,return 的那个函数类型和直接放在变量位置 
<T>的类型完全等价,所以协变 - 参数类型时,需要考虑的 block 是在等号后面函数,也就是说要在函数调用的过程中去看相当于 
Animal赋值给Dog(因为 paramAnimal 接受的参数是 Animal 类型),那显然是不安全的(Dog 是比 Animal 更加具体的类型,访问的属性在 Animal 中不存在),但反过来是安全的。