Skip to content

函数的协变与逆变

什么是协变与逆变

在 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;

为什么需要协变和逆变

这种设计确保了类型安全性:

  1. 参数逆变:确保传入的参数可以安全地被函数使用
  2. 返回值协变:确保函数返回的值可以安全地被调用方使用

双向协变(Bivariance)

在 TypeScript 早期版本中,函数参数默认是双向协变的,这可能导致类型安全问题。现在可以通过 strictFunctionTypes 编译选项来控制这个行为:

typescript
// 当 strictFunctionTypes: false 时(不推荐)
// 这种赋值在旧版本中是允许的
let f5: (animal: Animal) => void = dogCallback; // 在严格模式下会报错

实际应用

理解协变和逆变对于以下场景特别重要:

  1. 回调函数的类型定义
  2. 事件处理器的类型检查
  3. 泛型约束的设计
  4. 库的 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;