← 返回题目列表

TypeScript 中协变、逆变和双变是什么意思?

困难 第 28 / 32 题 更新于 2026/07/29
TypeScript类型兼容函数类型

简化版

协变表示类型关系方向保持一致,常见于只读返回值;逆变表示类型关系方向反过来,常见于函数参数;双变表示两个方向都允许,是 TypeScript 为兼容部分回调场景保留的宽松行为。

理解它的关键是函数类型是否安全:返回更具体的值通常安全,参数随便变窄可能不安全。开启 strictFunctionTypes 后,普通函数参数检查会更严格。

详细版

type Animal = { name: string };
type Dog = Animal & { bark(): void };

let dogs: Dog[] = [];
let animals: Animal[] = dogs; // 数组在 TS 中有历史上的协变表现

函数返回值协变:

type GetAnimal = () => Animal;
type GetDog = () => Dog;

const getDog: GetDog = () => ({ name: "d", bark() {} });
const getAnimal: GetAnimal = getDog; // 安全

函数参数逆变:

type HandleAnimal = (value: Animal) => void;
type HandleDog = (value: Dog) => void;

能处理所有 Animal 的函数,当然也能处理 Dog;只能处理 Dog 的函数,不一定能处理普通 Animal。

完整版教学

一、先从类型关系开始

假设 DogAnimal 的子类型。

Dog -> Animal

如果一个容器或函数包装后仍保持这个方向,就是协变;如果方向反过来,就是逆变。

方差不是 TypeScript 独有概念,它描述的是“包一层类型构造器后,子类型关系如何变化”。

二、返回值通常是协变

type Producer<T> = () => T;

如果你需要一个 Producer<Animal>,给你一个 Producer<Dog> 是安全的,因为 Dog 至少也是 Animal。

const dogProducer: Producer<Dog> = () => ({ name: "d", bark() {} });
const animalProducer: Producer<Animal> = dogProducer;

返回更具体的东西,使用方按更宽泛的类型接收,没有问题。

三、函数参数通常是逆变

type Consumer<T> = (value: T) => void;

如果某处会传入任意 Animal,你不能把只会处理 Dog 的函数塞进去。

const handleDog: Consumer<Dog> = dog => dog.bark();

如果把它当作 Consumer<Animal>,调用方传一只不会 bark 的猫就会出问题。

四、用表格记忆方向

位置方差倾向直觉
返回值协变产出更具体通常安全
参数逆变能接收更宽泛才安全
可读写容器复杂或不安全既读又写容易出洞

只读数组比可变数组更接近安全协变。

const dogs: readonly Dog[] = [];
const animals: readonly Animal[] = dogs;

五、双变来自兼容性妥协

TypeScript 为了兼容大量 JavaScript 回调写法,在某些方法参数位置保留双变行为。

这意味着参数类型检查比理论上更宽松。

interface Emitter {
  on(handler: (event: Animal) => void): void;
}

实际项目中,事件回调、组件 props、库类型定义都可能遇到这种宽松性。

六、strictFunctionTypes 的作用

strictFunctionTypes 会让函数参数检查更接近逆变。

type Fn<T> = (value: T) => void;

开启后,普通函数类型赋值会更严格,减少“只会处理 Dog 的函数被当成 Animal 处理器”的风险。

但方法签名仍有兼容性差异,所以不能简单认为所有地方都完全严格。

七、常见误区与追问

  • 误区:子类型赋值在任何位置都安全。 放到函数参数位置时方向可能反过来。
  • 误区:协变、逆变只影响 class。 函数类型、泛型容器、回调都受影响。
  • 误区:返回值和参数方差一样。 返回值通常协变,参数通常逆变。
  • 误区:strictFunctionTypes 能消除所有双变。 方法参数等位置仍有兼容性设计。
  • 追问:为什么只会处理 Dog 的函数不能当 Animal 处理器? 因为调用方可能传入非 Dog 的 Animal。
  • 追问:readonly 数组为什么更安全? 它不能写入更宽泛元素,减少可变容器的类型漏洞。

八、加强记忆

记忆图:

Dog <: Animal
Producer<Dog> <: Producer<Animal>      covariant
Consumer<Animal> <: Consumer<Dog>      contravariant

面试回答时先讲 Dog 和 Animal,再分别套返回值和参数,最后补 TypeScript 的兼容性妥协和 strictFunctionTypes