1. 类型系统里的双面镜:协变与逆变初探
第一次接触TypeScript类型系统时,很多人会被readonly Array<Animal>不能赋值给readonly Array<Dog>的现象搞糊涂。这背后其实是类型关系中的协变(covariance)与逆变(contravariance)在起作用。简单来说:
- 协变保持类型关系的方向(子类型可替代父类型)
- 逆变反转类型关系的方向(父类型可替代子类型)
在TypeScript中,函数参数类型就是典型的逆变场景。这意味着一个接收Animal参数的函数,可以被当作接收Dog参数的函数来使用。这看似违反直觉的设计,其实蕴含着深刻的类型安全哲学。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从现实场景看函数参数逆变
2.1 一个动物喂养的案例
假设我们有以下类型层次:
typescript复制class Animal { eat() {} }
class Dog extends Animal { bark() {} }
class Cat extends Animal { meow() {} }
现在考虑两个函数类型:
typescript复制type Feeder<T> = (animal: T) => void;
const feedAnimal: Feeder<Animal> = (a) => a.eat();
const feedDog: Feeder<Dog> = (d) => { d.eat(); d.bark(); };
根据逆变规则,Feeder<Animal>实际上是Feeder<Dog>的子类型。这意味着:
typescript复制let dogFeeder: Feeder<Dog> = feedAnimal; // ✅ 安全
// let animalFeeder: Feeder<Animal> = feedDog; // ❌ 危险
2.2 类型安全的反直觉设计
为什么允许父类型函数赋值给子类型变量是安全的?因为在执行feedDog(d)时:
- 实际调用的是
feedAnimal - 它只需要参数有
eat方法 - 所有
Dog实例都满足这个要求
反过来如果允许feedDog赋值给Feeder<Animal>,当传入Cat实例时就会尝试调用不存在的bark方法。
3. 类型系统的数学基础
3.1 范畴论视角
在范畴论中,函数类型构造器可以看作一个逆变函子(contravariant functor)。对于任意两个类型A和B:
- 如果A ≤ B(A是B的子类型)
- 那么
(B => C) ≤ (A => C)
这种关系反转正是逆变的数学本质。
3.2 里氏替换原则的延伸
面向对象的LSP原则指出:子类型必须能够替换父类型。函数参数的逆变可以看作这个原则在函数类型中的延伸:
- 参数类型要求更宽松(父类型)
- 返回值类型要求更严格(子类型)
这样组合使用时才能保证类型安全。
