"协变"(Covariance)和"逆变"(Contravariance)听起来像学术黑话,但作为 TypeScript 开发者,你在日常编码中其实每天都在和它们打交道——只是你可能不知道它们叫什么。本文将用实际代码讲清楚这两个概念,以及它们如何影响你写的每一行类型标注。
一、什么是型变(Variance)?
型变描述的是:当类型之间有父子关系时,由这些类型复合而成的更复杂类型之间是否保持同样的父子关系。
简单来说,如果 Cat 是 Animal 的子类型,那么:
Array<Cat>是Array<Animal>的子类型吗?(协变问题)(cat: Cat) => void是(animal: Animal) => void的子类型吗?(逆变问题)
二、协变(Covariance):保持父子关系方向
定义:如果 A 是 B 的子类型,那么 F<A> 也是 F<B> 的子类型。方向不变,这叫协变。
2.1 数组是协变的
class Animal {
name: string = 'animal';
eat() { console.log('eating...'); }
}
class Cat extends Animal {
meow() { console.log('meow!'); }
}
class Dog extends Animal {
bark() { console.log('woof!'); }
}
// Cat 是 Animal 的子类型
// Array<Cat> 也是 Array<Animal> 的子类型(协变)
const cats: Cat[] = [new Cat(), new Cat()];
const animals: Animal[] = cats; // ✅ OK:Array 是协变的
// 你可以遍历 animals 调用 Animal 上存在的方法
animals.forEach(animal => animal.eat()); // ✅ OK
// ⚠️ 但注意:协变数组也有安全隐患
// animals 实际指向 cats 数组,但你通过 animals 可以放入 Dog
animals.push(new Dog()); // TypeScript 不会报错,但运行时 cats[2].meow() 会出错!为什么 TypeScript 允许这样? 这是为了兼容 JavaScript 的灵活性。TypeScript 选择了"结构类型系统"(Structural Typing),数组和属性默认是协变的。可以通过配置
strictFunctionTypes部分缓解,但数组协变是刻意的设计权衡。
2.2 对象属性是协变的
interface AnimalShelter {
resident: Animal;
}
interface CatShelter {
resident: Cat;
}
// CatShelter 是 AnimalShelter 的子类型(协变)
// 因为 read 操作总是安全的:你期望得到 Animal,实际得到 Cat(也是 Animal)
const shelter: AnimalShelter = {
resident: new Cat()
}; // ✅ Cat 可以赋值给 Animal 类型的位置2.3 返回值是协变的
// 返回值位置:协变
// 如果 Cat 是 Animal 的子类型,那么 () => Cat 是 () => Animal 的子类型
type AnimalFactory = () => Animal;
type CatFactory = () => Cat;
const getCat: CatFactory = () => new Cat();
const getAnimal: AnimalFactory = getCat; // ✅ OK:返回值协变
// 调用 getAnimal,期望返回 Animal,实际返回 Cat —— 完全没问题
const pet: Animal = getAnimal();三、逆变(Contravariance):反转父子关系方向
定义:如果 A 是 B 的子类型,那么 F<B> 是 F<A> 的子类型。方向反转,这叫逆变。
逆变最常出现在函数参数位置。
3.1 函数参数是逆变的
// 函数参数位置:逆变
// 如果 Cat 是 Animal 的子类型,那么 (animal: Animal) => void 是 (cat: Cat) => void 的子类型
type AnimalHandler = (animal: Animal) => void;
type CatHandler = (cat: Cat) => void;
// 处理 Animal 的函数,可以处理 Cat(因为 Cat 也是 Animal)
const handleAnimal: AnimalHandler = (animal: Animal) => {
console.log(`Handling: ${animal.name}`);
animal.eat(); // 只调用 Animal 上存在的方法
};
// ✅ OK:(animal: Animal) => void 可以赋值给 (cat: Cat) => void
const handleCat: CatHandler = handleAnimal;
// 使用时:传入 Cat,handleAnimal 只访问了 Animal 上的属性/方法 → 完全安全
handleCat(new Cat()); // 输出:Handling: animal / eating...逆变直觉理解:
- 一个能处理任何动物的处理函数,当然也能处理猫(猫也是动物)
- 所以
Animal → void是Cat → void的子类型 - 参数类型越宽泛,函数越安全(作为被赋值的对象)
3.2 TypeScript 的 strictFunctionTypes
默认情况下,TypeScript 通过 --strictFunctionTypes(或 --strict)启用严格的函数参数逆变检查:
// strictFunctionTypes: true(推荐)
type Logger = (item: Animal) => void;
const catLogger: (item: Cat) => void = (cat: Cat) => {
cat.meow(); // 假设 Cat 有 meow 方法
};
// ❌ 错误!如果关闭 strictFunctionTypes 则允许
// const logger: Logger = catLogger;
// 危险:logger(new Dog()) 时会调用不存在的 meow()四、TypeScript 中的型变速查
| 类型位置 | 型变方向 | 规则 | 示例 |
|---|---|---|---|
| 数组元素 | 协变 | Array<Cat> → Array<Animal> |
let a: Animal[] = cats |
| 对象属性(只读) | 协变 | {v: Cat} → {v: Animal} |
let a: {v: Animal} = {v: cat} |
| 函数返回值 | 协变 | () => Cat → () => Animal |
let f: () => Animal = () => cat |
| 函数参数 | 逆变 | (a: Animal)=>void → (c: Cat)=>void |
let h: CatHandler = animalHandler |
| Promise | 协变 | Promise<Cat> → Promise<Animal> |
let p: Promise<Animal> = Promise.resolve(cat) |
| Map/Set | 协变 | Map<string, Cat> → Map<string, Animal> |
值的部分是协变 |
五、实际场景
5.1 事件处理器中的逆变
// DOM 事件处理是逆变的最佳实践体现
type ClickHandler = (event: MouseEvent) => void;
type GenericHandler = (event: Event) => void;
// GenericHandler(参数更宽泛)可以赋值给 ClickHandler(参数更具体)
// 因为当用户点击时,传入的确实是 MouseEvent,而 GenericHandler 只要求 Event
const logEvent: GenericHandler = (event) => {
console.log(event.type, event.timeStamp);
};
// ✅ 逆变使得这成为可能
button.addEventListener('click', logEvent);
// addEventListener('click', handler: (e: MouseEvent) => void)
// GenericHandler 是 (e: MouseEvent) => void 的子类型(参数逆变)5.2 回调函数中的逆变
// 实际场景:通用的数组处理函数
function processItems<T>(
items: T[],
callback: (item: T) => void
): void {
items.forEach(callback);
}
const logAny = (item: unknown) => {
console.log('Processing:', item);
};
const numbers = [1, 2, 3];
// ✅ unknown 比 number 更宽泛,所以 (item: unknown) => void 是 (item: number) => void 的子类型
processItems(numbers, logAny); // 逆变在起作用
// 同理,这也很安全
const strings = ['a', 'b', 'c'];
processItems(strings, logAny); // ✅5.3 Array 方法中的协变与逆变
// map:参数逆变 + 返回值协变
const animals: Animal[] = [new Cat(), new Dog(), new Cat()];
// map 签名:map<U>(callback: (value: T, ...) => U): U[]
// value 参数是逆变的,返回值是协变的
// ✅ callback 参数更宽泛(逆变)
const names: string[] = animals.map((animal: Animal) => animal.name);
// ✅ 返回值更具体(协变)
const cats: Cat[] = [new Cat(), new Cat()];
const pets: Animal[] = cats.map(cat => cat); // Cat → Animal 协变
// filter:参数逆变
// filter(predicate: (value: T, ...) => boolean): T[]
const isAnimal = (x: { name: string }) => true; // 参数更宽泛
const filtered = animals.filter(isAnimal); // ✅ 逆变使得这可以工作5.4 回调参数的双向协变(TypeScript 特殊设计)
// TypeScript 对回调参数的检查比一般函数参数更宽松
// 这是为了兼容常见 JavaScript 模式
interface AnimalEvent {
onEat(callback: (animal: Animal) => void): void;
}
const handler = (cat: Cat) => {
cat.meow();
};
// 在 strictFunctionTypes 为 true 时,以下代码的行为:
const shelter: AnimalEvent = {
// ⚠️ 这里可能会有类型错误,取决于 TypeScript 版本和配置
onEat(callback) {
callback(new Cat());
}
};注意:
strictFunctionTypes只对非方法签名(属性形式的函数类型)生效。类方法中的参数仍然是双向协变的。
六、记住型变的实用法则
🍎 协变(Covariance):输出位置 → 方向不变
"猫是动物 → 猫工厂也是动物工厂"
适用于:返回值、数组元素、Promise 值、只读属性
🔃 逆变(Contravariance):输入位置 → 方向反转
"能处理任何动物的函数 → 也能处理猫"
适用于:函数参数
📦 不变(Invariance):输入+输出位置 → 没有子类型关系
适用于:读写属性、mutable 字段七、总结
- 协变保持方向:子类型在哪里都是子类型(返回值、数组、只读属性)
- 逆变反转方向:参数类型越宽泛,函数越"小"(作为子类型)
- TypeScript 的设计在类型安全和JavaScript 兼容性之间做了平衡
- 理解型变能帮你写出更准确的泛型约束,避免"为什么这里报类型错误"的困惑
- 开启
strictFunctionTypes获得更安全的函数参数逆变检查
下次当你在 TypeScript 中疑惑"为什么这个函数能赋值给那个类型"时,想想是协变还是逆变在起作用,答案往往一目了然。