一、 父子类型
先来看一个例子:
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会检查到错误:
结论就是: 函数参数的协变是不安全的,函数参数的逆变是安全的。
四、总结
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