为何TypeScript中函数参数具有逆变特性?
函数参数逆变的原理:从类型安全出发的必然规则
先拿具体的类型例子拆解,避免抽象定义的迷惑:
class Animal { eat() {} } class Dog extends Animal { bark() {} }
这里Dog是Animal的子类型(Dog extends Animal),对应你说的T extends U(T=Dog,U=Animal)。
先看协变(做对比)
协变多见于产出数据的类型,比如数组:Dog[]可以赋值给Animal[]。因为从数组里拿出来的都是Dog,肯定能当Animal用,不会出问题,完全符合你说的协变规则:T extends U → G<T> extends G<U>(G是数组类型)。
函数参数为什么是逆变?
函数是接收输入、返回输出的逻辑块,参数属于「输入」位置,类型规则必须保证:赋值后的函数调用不会触发类型错误。
定义两个函数类型:
type AnimalHandler = (animal: Animal) => void; type DogHandler = (dog: Dog) => void;
按照逆变规则,因为Dog extends Animal,所以AnimalHandler可以赋值给DogHandler类型的变量——这看起来反直觉,但实际调用是安全的:
// 把AnimalHandler赋值给DogHandler变量 let handler: DogHandler = (animal: Animal) => { animal.eat(); // 只用到Animal的公共方法 }; // 调用时传入Dog,完全没问题 handler(new Dog());
函数只依赖Animal的属性,Dog作为子类型肯定具备这些属性,所以调用安全。
如果反过来,允许把DogHandler赋值给AnimalHandler:
let handler: AnimalHandler = (dog: Dog) => { dog.bark(); // 调用Dog特有的方法 }; // 调用时传入Animal,直接报错 handler(new Animal()); // Animal没有bark方法
这就直接破坏了类型安全,所以这种赋值是不被允许的——这就是逆变规则存在的核心原因:保证函数调用时,参数的实际类型不会超出函数能处理的范围。
本质:输入输出的方差逻辑
函数类型的方差规则是行业通用的(不止TypeScript,Java、C#等都遵循):
- 返回值(输出位置)是协变的:比如
() => Dog可以赋值给() => Animal,因为返回的Dog能当作Animal用,和数组协变逻辑一致。 - 参数(输入位置)是逆变的:接受更宽泛类型的函数,必然能处理更具体的子类型输入,不会触发错误。
这本质是对「里氏替换原则」的延伸:子类型可以替换父类型,但处理父类型的逻辑,一定能兼容子类型的输入。
内容的提问来源于stack exchange,提问作者LeoPkm2-1
相关产品推荐
相关产品推荐

