TypeScript类型系统中的协变与逆变位置规则
建立类型层级模型。要理解位置规则,首先需要定义一组存在继承关系的类型。定义一个基类 Animal 和一个派生类 Dog。
class Animal {
name: string;
}
class Dog extends Animal {
bark(): void {}
}
在这个模型中,Dog 是 Animal 的子类型。这意味着在任何期望 Animal 的地方,都可以安全地提供 Dog。
观察返回值位置的协变性。当一个函数作为返回值时,其类型的兼容性保持原方向,这被称为协变。
定义两个函数类型:
type ReturnAnimalFn = () => Animal;
type ReturnDogFn = () => Dog;
验证赋值操作:
let makeDog: ReturnDogFn = () => new Dog();
let makeAnimal: ReturnAnimalFn = makeDog; // 允许赋值
分析逻辑:调用 makeAnimal 时期望得到一个 Animal,而 makeDog 实际返回的是 Dog(也是一种 Animal)。这是安全的。因此,返回值位置允许“子类型赋值给父类型”,即协变。
分析参数位置的逆变性。当一个函数作为参数时,其类型的兼容性方向是相反的,这被称为逆变。
定义两个接收参数的函数类型:
type TakeAnimalFn = (arg: Animal) => void;
type TakeDogFn = (arg: Dog) => void;
尝试反向赋值:
let processAnimal: TakeAnimalFn = (animal) => console.log(animal.name);
let processDog: TakeDogFn = processAnimal; // 允许赋值吗?
推导安全性:
processDog的类型签名承诺:它只处理Dog。- 实际上
processDog指向了processAnimal。 processAnimal可以处理任何Animal,自然也能处理Dog。- 结论:把一个能处理
Animal的函数,赋值给一个只需要处理Dog的变量是安全的。
对比错误情况(如果反向赋值会怎样):
let dangerousDog: TakeDogFn = (dog) => dog.bark();
let dangerousAnimal: TakeAnimalFn = dangerousDog; // 编译器报错(在严格模式下)
分析风险:
dangerousAnimal的类型签名承诺:它处理任何Animal。- 如果调用者传入了一只
Cat(也是一种Animal)。 - 实际执行的是
dangerousDog,它会尝试调用cat.bark()。Cat没有bark方法,程序崩溃。
因此,参数位置只能接受“父类型赋值给子类型”,即参数类型必须更宽泛。这与子类型化方向相反,称为逆变。
识别参数与返回值的综合关系。对于一个完整的函数类型 Source 赋值给 Target,规则如下:
使用数学公式表示:
$$
S_{in} \supseteq T_{in} \quad \text{(参数逆变:参数类型 S 比 T 更宽泛)}
$$
$$
S_{out} \subseteq T_{out} \quad \text{(返回值协变:返回类型 S 比 T 更具体)}
$$
应用以下流程来判断函数赋值是否合法:
Source #gt;= Target?"} Check1 -->|Yes| ParamYes["参数安全: 逆变满足"] Check1 -->|No| Fail1["报错: 参数不兼容"] ParamYes --> Step2["检查返回值位置"] Step2 --> Check2{"返回类型
Source #lt;= Target?"} Check2 -->|Yes| ReturnYes["返回值安全: 协变满足"] Check2 -->|No| Fail2["报错: 返回值不兼容"] ReturnYes --> Success["赋值成功"]
配置严格检查模式。TypeScript 在处理方法(Methods)时,为了兼容旧版 JavaScript,默认启用了双向协变,即参数既允许协变也允许逆变。这是不安全的。
启用 strictFunctionTypes 选项。在 tsconfig.json 中将此选项设为 true。
{
"compilerOptions": {
"strictFunctionTypes": true
}
}
区分方法与函数属性。该选项仅对函数类型声明有效,对对象中的方法声明无效。
测试对象方法的特殊情况:
interface Comparator {
compare: (a: Animal, b: Animal) => number; // 函数属性:应用严格逆变检查
}
interface ComparatorMethod {
compare(a: Animal, b: Animal): number; // 方法声明:允许双向协变(为了灵活性,暂未严格逆变)
}
注意:虽然方法声明目前保留了双向协变以保证旧代码兼容,但在编写新代码时,应尽量避免依赖这种不安全的特性。优先使用函数属性而非方法声明,或者手动确保参数类型的正确性。
总结排查步骤。当遇到类型错误 Type 'X' is not assignable to type 'Y' 时,按以下步骤排查:
- 定位错误发生是在参数列表中还是在返回值处。
- 如果是参数错误:
- 检查源类型的参数是否比目标类型的参数更宽泛(父类)。
- 如果不是,修改目标类型,使其参数接受更宽泛的类型。
- 如果是返回值错误:
- 检查源类型的返回值是否比目标类型的返回值更具体(子类)。
- 如果不是,修改源类型,确保它返回符合目标约束的类型。
- 确认
tsconfig.json中已开启strict或strictFunctionTypes,以防止不安全的双向协变。

暂无评论,快来抢沙发吧!