TypeScript 协变与逆变
协变与逆变描述"子类型能否出现在父类型的位置"。TypeScript 是结构类型系统,大多数位置保持协变(子类实例可赋给父类变量),函数参数位置则讲究逆变。理解可变性,才能写出既灵活又安全的泛型与回调。
子类型与赋值兼容(协变)
若 Dog 是 Animal 的子类型,那么 Dog 可以出现在任何期望 Animal 的位置:
class Animal {
eat(): void {
console.log("吃东西");
}
}
class Dog extends Animal {
bark(): void {
console.log("汪汪");
}
}
let a: Animal = new Dog(); // ✓ 协变:子类实例赋给父类变量
a.eat(); // 输出:吃东西
数组在 TypeScript 中同样是协变的(实用优先的取舍):
const dogs: Dog[] = [new Dog()];
const animals: Animal[] = dogs; // ✓ 编译通过(协变)
返回类型协变
"返回 Dog 的函数"可以赋值给"要求返回 Animal 的函数":
type Producer<T> = () => T;
const produceDog: Producer<Dog> = () => new Dog();
const producer: Producer<Animal> = produceDog; // ✓ 更具体的返回类型总是安全
调用方只要求拿到 Animal,实际拿到 Dog 当然满足。
参数位置逆变
参数方向相反:能处理"所有 Animal"的函数,可以替换掉"只处理 Dog"的函数(能者多劳);反过来则危险:
type Consumer<T> = (arg: T) => void;
const eatAll: Consumer<Animal> = (x: Animal) => x.eat();
const eatDog: Consumer<Dog> = eatAll; // ✓ 逆变:参数更宽的可替代参数更窄的
// 反例:期望处理 Animal,却只写 Dog 参数的函数
// const bad: Consumer<Animal> = (x: Dog) => x.bark();
// 编译错误:传入位置可能是 Cat,Cat 没有 bark
方法参数的"双变"例外
开启 strictFunctionTypes 后,上面规则只对"函数类型"生效;对象里以方法语法声明的成员,参数仍按双变(双向都允许)比较:
interface Feeder {
feed(a: Animal): void; // 方法语法
}
class DogFeeder implements Feeder {
feed(dog: Dog): void { // ✓ 双变:方法参数窄化也能通过编译
dog.bark();
}
}
若想严格限制,把方法声明成函数属性即可:feed: (a: Animal) => void。
小结
赋值看协变(子给父安全),函数返回看协变、参数看逆变,方法语法则有双变例外。写回调与泛型时尽量让参数类型宽一些、返回类型窄一些,类型系统会因此更安全、复用性更好。