函数的协变与逆变
什么是协变与逆变
在 TypeScript 中,协变(Covariance)和逆变(Contravariance)是描述类型转换后的类型兼容性的概念。
- 协变:允许子类型赋值给父类型,即类型转换方向与继承层次一致
- 逆变:允许父类型赋值给子类型,即类型转换方向与继承层次相反
函数参数的逆变性
函数参数具有逆变性,这意味着父类型可以赋值给子类型:
typescript
interface Animal {
name: string;
}
interface Dog extends Animal {
bark(): void;
}
let animalCallback = (animal: Animal) => {
console.log(animal.name);
};
let dogCallback = (dog: Dog) => {
console.log(dog.name);
dog.bark();
};
// ✅ 合法:函数参数是逆变的
let f1: (dog: Dog) => void = animalCallback;
// ❌ 不合法:因为 dogCallback 需要 bark 方法
let f2: (animal: Animal) => void = dogCallback;
函数返回值的协变性
函数返回值具有协变性,这意味着子类型可以赋值给父类型:
typescript
let getAnimal = (): Animal => ({ name: 'animal' });
let getDog = (): Dog => ({ name: 'dog', bark: () => console.log('woof') });
// ❌ 不合法:返回值类型不够具体
let f3: () => Dog = getAnimal;
// ✅ 合法:函数返回值是协变的
let f4: () => Animal = getDog;
为什么需要协变和逆变
这种设计确保了类型安全性:
- 参数逆变:确保传入的参数可以安全地被函数使用
- 返回值协变:确保函数返回的值可以安全地被调用方使用
双向协变(Bivariance)
在 TypeScript 早期版本中,函数参数默认是双向协变的,这可能导致类型安全问题。现在可以通过 strictFunctionTypes
编译选项来控制这个行为:
typescript
// 当 strictFunctionTypes: false 时(不推荐)
// 这种赋值在旧版本中是允许的
let f5: (animal: Animal) => void = dogCallback; // 在严格模式下会报错
实际应用
理解协变和逆变对于以下场景特别重要:
- 回调函数的类型定义
- 事件处理器的类型检查
- 泛型约束的设计
- 库的 API 设计
typescript
// 示例:事件处理器
interface Event {
timestamp: number;
}
interface MouseEvent extends Event {
x: number;
y: number;
}
type EventHandler<E extends Event> = (event: E) => void;
let generalHandler: EventHandler<Event> = (e: Event) =>
console.log(e.timestamp);
let mouseHandler: EventHandler<MouseEvent> = (e: MouseEvent) =>
console.log(e.x, e.y);
// ✅ 合法:参数逆变
let h1: EventHandler<MouseEvent> = generalHandler;
// ❌ 不合法
let h2: EventHandler<Event> = mouseHandler;