ts中的协变和逆变

107 阅读2分钟

一、 父子类型

先来看一个例子:

interface Animal {
  name: string;
  age: number;
}

interface Dog extends Animal {
  bark: string
}

上面的代码中Dog类型继承了Animal类型,并拓展了一个bark类型,那么能否将dog类型赋值给Animal类型呢?

const dog: Dog = {
  name: "jack",
  age: 2,
  bark: "汪汪汪",
 }

const animal: Animal = dog;

答案是可以的,这是因为 extends关键字可以继承父类的类型并进行属性拓展,也就是父类中有的属性子类中一定有,子类的类型是包含父类的,当我们把Dog类型赋值给Animal类型的时候代码是正常执行的,因为Animal.name、Animal.age属性都是存在的不会影响js代码的执行。

二、协变

如果A是B的子类型,则T<A>也是T<B>的子类型。即子类型关系和原始类型关系一致。 示例:

// 数组是协变的
const dogs: Dog[] = [{name:"jack", age:21, bark:"汪汪"}];
let animals: Animal[] = dogs;

// 函数返回值是协变的
type GetAnimal = () => Animal;
type GetDog = () => Dog;

const getDog: GetDog = ()=> ({name:"jack", age:21, bark:"汪汪"});
const getAnimal: GetAnimal = getDog;

三、逆变

如果A是B的子类型,则T<B>T<A>的子类型。子类关系和原始类型关系翻转。 示例:

type AnimalHandler = (a: Animal) => void;
type DogHandler = (a: Dog) => void;

// 在严格模式(strictFunctionTypes)下,函数参数是逆变的
const animalHandler: AnimalHandler = (a: Animal) => { };
const dogHandler: DogHandler = animalHandler;

// 反向赋值会报错(不安全)
animalHandler = dogHandler; // ❌ 严格模式下禁止

默认情况下函数参数是双向的,同时支持协变和逆变, 当开启严格模式时,函数参数是只支持逆变的,这是因为函数参数的协变“不够安全”

const dogHandler: DogHandler = (a: Dog) => {
  console.log(a.name, a.age, a.bark);
};
const animalHandler: AnimalHandler = dogHandler;

// a.bark属性是undefined
animalHandler({ name: "tom", age: 1 });

而函数参数逆变的时候,如果使用了Dog的bark属性,ts会检查到错误:

image.png

结论就是: 函数参数的协变是不安全的,函数参数的逆变是安全的。

四、总结

JavaScript的动态特性对静态的类型系统的设计是一个巨大的挑战,TS类型系统的实现在安全和灵活性之间做了大量的权衡,而协变和逆变正是这一权衡的产物:

  • 协变按确保类型安全的基础上,增加了类型的灵活性
  • 逆变确保了函数参数类型的安全性

协变逆变触发条件:

  • 数组、函数返回值是协变的
  • 函数参数在默认模式下双向协变(同时支持协、逆变)的
  • 函数参数在严格模式(strictFunctionTypes)下是逆变的

五、拓展

数组的协变一定是安全的吗?

const dogs: Dog[] = [{ name: "jack", age: 1, bark: "汪汪汪" }];
const animals: Animal[] = dogs;
animals.push({ name: "Tom", age: 1.2 });

console.log(dogs[1].bark); // undefined