协变与逆变的概念
协变与逆变的概念
一句话概括
协变 = 子类型关系顺着传,逆变 = 子类型关系反着传——返回值协变(可以返回更具体的)、参数逆变(可以接受更宽泛的)。搞混了这两条,函数类型兼容性永远靠猜。
核心知识点
1. 先建立直觉:子类型可以塞给父类型
1
2
3
4
class Animal { name = ""; }
class Dog extends Animal { breed = ""; }
const a: Animal = new Dog(); // ✅ Dog 可以当 Animal 用
2. 协变:返回值可以”更具体”
1
2
3
4
5
6
7
8
type Factory<T> = () => T;
const dogFactory: Factory<Dog> = () => new Dog();
const animalFactory: Factory<Animal> = dogFactory; // ✅
// 说好了返回 Animal,实际返回 Dog → 安全(Dog 拥有 Animal 所有属性)
// 反过来不行:
// const wrong: Factory<Dog> = () => new Animal(); // ❌ 说返回 Dog,实际返回 Animal
规则:输出位置(返回值)= 协变。子类型方向相同。
3. 逆变:参数可以”更宽泛”
1
2
3
4
5
6
7
8
type Handler<T> = (x: T) => void;
const animalHandler: Handler<Animal> = (a: Animal) => console.log(a.name);
const dogHandler: Handler<Dog> = animalHandler; // ✅(strictFunctionTypes 开启时)
// 说好了传 Dog,实际函数能处理 Animal → 安全(Dog 也是 Animal)
// 反过来不行:
// const wrong: Handler<Animal> = (d: Dog) => d.bark(); // ❌ 说处理 Animal,实际只能处理 Dog
规则:输入位置(参数)= 逆变。子类型方向相反。
4. 不变:同时输入又输出 = 必须完全匹配
1
2
3
4
5
6
7
8
9
interface Box<T> {
value: T;
}
// Box<Dog> 和 Box<Animal> 互不兼容
// 因为 value 既可读(协变要求)又可写(逆变要求)→ 必须不变
const db: Box<Dog> = { value: new Dog() };
// const ab: Box<Animal> = db; // ❌ 如果允许这行:
// ab.value = new Cat(); // db.value 现在是 Cat!类型炸了
5. TypeScript 的实际行为——双变陷阱
1
2
3
4
5
6
7
8
9
10
// 默认情况下(non-strict),函数参数是双变(bivariant)
let f1: (x: Dog) => void = (x: Animal) => {}; // ✅ 逆变安全
let f2: (x: Animal) => void = (x: Dog) => {}; // ✅ 双变允许(但实际上不安全!)
// 开启 strictFunctionTypes 后,f2 会报错
// 接口中的方法声明永远是双变(历史兼容),函数类型声明是逆变
interface Compare<T> {
compare(a: T, b: T): number; // 方法 → 双变
}
type CompareFn<T> = (a: T, b: T) => number; // 函数类型 → strict 下逆变
「其实你每天都在用」
- 数组是协变的:
const animals: Animal[] = dogs✅——代价是animals.push(new Cat())不会报类型错(运行时会炸) - 事件处理器:
onChange: (e: ChangeEvent) => void接受(e: Event) => void的处理器——参数逆变,安全 - Promise 是协变的:
Promise<Dog>可以赋给Promise<Animal> - Redux reducer 参数:
(state, action: Action)可以接受子类型 action——逆变确保安全 - ReadonlyArray 是安全的协变:只读数组天然协变且安全,因为不能 push
常见误解(FAQ)
❌ 误区 1:TypeScript 严格模式下所有函数参数都逆变
接口/类中声明的方法参数永远是双变(bivariant),只有函数类型声明(type Fn = (x: T) => …)的参数在 strictFunctionTypes 下才逆变。这是为了兼容 JS 生态的故意设计。
❌ 误区 2:Dog[] extends Animal[] 所以可以安全赋值
TypeScript 默认数组是协变的,但不安全。const animals: Animal[] = [new Dog()]; animals.push(new Cat()) 不会报类型错。要安全用 ReadonlyArray<T>。
❌ 误区 3:逆变太反直觉,跟我写业务没关系
写 Redux/zustand 的 selector、React 的事件处理、Vue 的 emit 类型推导——底层全是协变/逆变。不理解它们,遇到 Type 'X' is not assignable to type 'Y' 就只能瞎试。
❌ 误区 4:strictFunctionTypes 开了就行,不用理解原理
开了 strictFunctionTypes 只对函数类型声明生效,方法声明仍然双变。遇到方法声明参数的类型兼容性报错不报错,不搞清楚规则就想不通。
一句话总结
“你给我什么,我按更宽的取;我要你给我什么,你得给更细的”——记住这句话,协变和逆变就不乱了。
本文由作者按照 CC BY 4.0 进行授权