不管是逆变还是协变,其最终原理都只有一个:子类型可以隐式转换为父类型,或者说子类型可以隐式赋值给父类型。
协变
协变很好理解,就是将子类型的变量赋值给父类型的变量:
interface Animal {
name: string;
age: number;
}
interface Dog {
name: string;
age: number;
bite(): void;
}
let animal: Animal;
let wangCai: Dog = {
name: "wang cai",
age: 3,
bite: () => {},
};
animal = wangCai;
这里的 animal 是 Animal 类型,wangCai 是 Dog 类型,Dog 类型是 Animal 类型的子类型,将 wangCai 赋值给 animal 是不会报错的。这种子类型可以赋值给父类型的情况,称之为协变。
逆变
逆变比较难理解,一般出现在函数的参数中。先看一个例子:
interface Animal {
name: string;
age: number;
}
interface Dog {
name: string;
age: number;
bite(): void;
}
let animalFun: (animal: Animal) => void;
animalFun = (animal: Animal) => {
console.log(animal.name);
}
let dogFun: (dog: Dog) => void;
dogFun = (dog: Dog) => {
dog.bite();
}
dogFun = animalFun;
animalFun = dogFun;
上面函数 animalFun 接收 Animal 类型的参数,而函数 dogFun 接收 Dog 类型的参数,现在将 animalFun 赋值给 dogFun,或者将 dogFun 赋值给 animalFun,编译器会报错吗?
结果如下:
发现将 animalFun 赋值给 dogFun 编译器并不会报错,而将 dogFun 赋值给 animalFun 编译器会提示错误。
我们知道,animalFun 接收 Animal 类型的参数;dogFun 接收 Dog 类型的参数。并且 Dog 类型为 Animal 类型的子类型。
也就是说拥有父类型参数的函数可以赋值给拥有子类型参数的函数;而拥有子类型参数的函数不可以赋值给拥有父类型参数的函数。这种现象就是逆变。为什么会出现这种情况呢?请看下面的分析:
- dogFun 赋值给 animalFun
我们知道,animalFun 在定义的时候,接收 Animal 类型的参数,同样在调用它的时候,也应该接收 Animal 类型的参数,或者接收 Animal 类型的子类型作为参数:
animalFun(animal); // ok
animalFun(dog); // ok
如果将 dogFun 赋值给 animalFun,则 animalFun 拥有了 dogFun 的实现,意味着 animalFun 需要接收 Dog 类型的参数:
let dogFun: (dog: Dog) => void;
let animalFun: (animal: Animal) => void;
dogFun = (dog: Dog) => {
dog.bite();
}
animalFun = dogFun;
这就可能造成类型的不安全,因为编译器允许我们给 animalFun 传 Animal 类型的参数。而此时,在 animalFun 的实现中,它需要接收 Dog 类型的参数:
// 实现
let animalFun = (dog: Dog) => {
dog.bite();
}
// 调用
animalFun(dog); // ok
animalFun(animal); // error
如果我们将 animal 当作参数传给 animalFun,肯定是会报错的。因为 animal 并非 Dog 类型,也非 Dog 类型的子类型。在 animalFun 中,调用了传入的参数的 bite 方法,而 animal 是没有 bite 方法的。
对于这种类型的不安全,TS 肯定得提前发现,不然 TS 就没有这么好的体验了。所以 TS 在哪里报错比较合理呢?在调用 animalFun(animal) 的时候吗?
不太好,因为我们在定义 animalFun 函数的时候,它是接收 Animal 类型的参数。而现在调用它的时候,给它传入 Animal 类型的参数,它却报错了。这明显不符合常理,而且也不利于定位问题。
所以最好就是在实现 animalFun 函数的时候,给它报错。提示我们,animalFun 在调用的时候可能会传入 Animal 类型的参数,你不能在实现的时候,限制它只能传入 Dog 类型的参数。
将 dogFun 赋值给 animalFun,看似是将父类型的参数赋值给子类型的参数,实际上在调用的时候,还是将子类型的参数赋值给父类型的参数。
- animalFun 赋值给 dogFun
类似地,dogFun 在定义的时候,接收 Dog 类型的参数,同样在调用它的时候,也应该接收 Dog 类型的参数,或者接收 Dog 类型的子类型作为参数:
dogFun(dog); // 狗
dogFun(collie); // 牧羊犬
如果将 animalFun 赋值给 dogFun,则 dogFun 拥有了 animalFun 的实现,意味着 dogFun 需要接收 Animal 类型的参数:
let animalFun: (animal: Animal) => void;
let dogFun: (dog: Dog) => void;
animalFun = (animal: Animal) => {
console.log(animal.name);
}
dogFun = animalFun;
此时类型的传递是安全的,因为编译器允许我们给 dogFun 传 Dog 类型的参数。而此时,在 dogFun 的实现中,它接收 Animal 类型的参数,传入的 Dog 类型是 Animal 类型的子类型:
// 实现
let dogFun = (animal: Animal) => {
console.log(animal.name);
}
// 调用
dogFun(dog); // ok
dogFun(collie); // collie 为 Dog 类型的子类型,当然也是 Animal 类型的子类型,ok
如果我们将 dog 当作参数传给 dogFun,这是没问题的。因为 dog 虽然不是 Animal 类型,但是却是 Animal 类型的子类型,Animal 类型所拥有的属性和方法 dog 都有,在 dogFun 函数中访问传入参数的 name 属性当然是可以访问到的。
所以它不会报错。
将 animalFun 赋值给 dogFun,看似是将父类型的参数赋值给子类型的参数,实际上在调用的时候,还是将子类型的参数赋值给父类型的参数。