文章目录

TypeScript类型系统中的协变与逆变位置规则

发布于 2026-05-17 22:15:00 · 浏览 29 次 · 评论 0 条

TypeScript类型系统中的协变与逆变位置规则

建立类型层级模型。要理解位置规则,首先需要定义一组存在继承关系的类型。定义一个基类 Animal 和一个派生类 Dog

class Animal {
  name: string;
}

class Dog extends Animal {
  bark(): void {}
}

在这个模型中,DogAnimal 的子类型。这意味着在任何期望 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; // 允许赋值吗?

推导安全性:

  1. processDog 的类型签名承诺:它只处理 Dog
  2. 实际上 processDog 指向了 processAnimal
  3. processAnimal 可以处理任何 Animal,自然也能处理 Dog
  4. 结论:把一个能处理 Animal 的函数,赋值给一个只需要处理 Dog 的变量是安全的。

对比错误情况(如果反向赋值会怎样):

let dangerousDog: TakeDogFn = (dog) => dog.bark();
let dangerousAnimal: TakeAnimalFn = dangerousDog; // 编译器报错(在严格模式下)

分析风险:

  1. dangerousAnimal 的类型签名承诺:它处理任何 Animal
  2. 如果调用者传入了一只 Cat(也是一种 Animal)。
  3. 实际执行的是 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 更具体)} $$

应用以下流程来判断函数赋值是否合法:

graph TD Start["开始赋值检查: SourceFn -> TargetFn"] --> Step1["检查参数位置"] Step1 --> Check1{"参数类型
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' 时,按以下步骤排查:

  1. 定位错误发生是在参数列表中还是在返回值处。
  2. 如果是参数错误
    • 检查源类型的参数是否比目标类型的参数更宽泛(父类)。
    • 如果不是,修改目标类型,使其参数接受更宽泛的类型。
  3. 如果是返回值错误
    • 检查源类型的返回值是否比目标类型的返回值更具体(子类)。
    • 如果不是,修改源类型,确保它返回符合目标约束的类型。
  4. 确认 tsconfig.json 中已开启 strictstrictFunctionTypes,以防止不安全的双向协变。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文