关于TS类型系统中的协变与逆变

在 TypeScript 类型系统中,类型兼容性是核心支柱,而协变逆变双变不变 则是描述类型转换规则的四大概念。它们决定了不同类型的集合(如数组、函数、泛型)之间如何安全地兼容,直接影响代码的类型安全性。但是之前一直很难理解协变与逆变,所以写本文记录一下。

一、类型兼容性

首先明确子类型父类型 的判定

子类型

TypeScript 中,子类型的核心判定依据是"赋值兼容性" :若类型 B 的所有可能值都能安全地赋值给类型 A 的变量,则 BA 的子类型(记为 B extends A)。通俗说,子类型的范围是父类型范围的"子集"

不同类型的子类型关系示例:

  1. 对象类型 :结构兼容即子类型
    B 包含 A 所有属性且类型匹配,则 BA 的子类型(B 的范围是 A 的子集)。
ts 复制代码
type Animal = { name: string }; // 父类型:所有含 name 的对象
type Dog = { name: string; bark: () => void }; // 子类型:范围更小(仅含 bark 的 Animal)
const dog: Dog = { name: "旺财", bark: () => {} };
const animal: Animal = dog; // 合法:Dog 是 Animal 的子集
  1. 联合类型 :范围收窄即子类型
    B 的所有成员都属于 A 的成员,则 BA 的子类型(B 范围更小)。
ts 复制代码
type NumOrStr = number | string; // 父类型:范围 {number, string}
type OnlyNum = number; // 子类型:范围 {number}(NumOrStr 的子集)
const num: OnlyNum = 123;
const numOrStr: NumOrStr = num; // 合法:number 是 number|string 的子集
  1. 基础类型 :字面量是基础类型的子类型
    字面量类型的范围是基础类型的子集(如 123number 的子集)。
ts 复制代码
type Age = number;
type SpecificAge = 18; // 子类型:范围仅 {18}
const age: Age = 18;
const specificAge: SpecificAge = 18;
const age2: Age = specificAge; // 合法:18 是 number 的子集

协变与逆变这两个术语源于数学中的范畴论(Category Theory) ,后来被计算机科学引入类型系统领域。物理学中也有类似的,简单来说就是"一个量随另一个量的变化,如果一致就是协变,否则就是逆变"(不太严谨),在类型系统中就是某个类型集合兼容性随某些类型变化是怎么样的?比如数组类型随其元素类型变化后的类型兼容性如何变化?函数类型随其参数类型或返回值类型变化后的类型兼容性如何?

二、协变:子类型关系的"正向传递"

协变 指:当 BA 的子类型时,B 的"容器类型"也是 A 的"容器类型"的子类型(即 F<B> extends F<A>)。简单说,容器的子类型关系与原类型"同向"

典型场景:

  1. 数组的协变
    B extends A,则 B[] extends A[](子类型数组是父类型数组的子类型)。
ts 复制代码
const dogs: Dog[] = [dog];
const animals: Animal[] = dogs; // 合法:Dog[] 是 Animal[] 的子类型(协变)
    • 安全性:从 animals 读取元素时,总能获得 Animal 所需的 name 属性(安全)。
    • 隐患:向 animals 写入非 Dog 类型(如 { name: "猫" })会破坏 dogs 的类型一致性(TS 未禁止,历史遗留问题)。
  1. 函数返回值的协变
    若函数返回 B(子类型),则该函数是返回 A(父类型)的函数的子类型。
ts 复制代码
const getDog = (): Dog => ({ name: "旺财", bark: () => {} });
type GetAnimal = () => Animal;
const getAnimal: GetAnimal = getDog; // 合法:返回 Dog 的函数是返回 Animal 的函数的子类型
    • 安全性:调用 getAnimal 时,返回值至少包含 Animal 所需的 name(安全)。

三、逆变:子类型关系的"反向传递"

逆变 指:当 BA 的子类型时,A 的"函数类型"是 B 的"函数类型"的子类型(即 (a: A) => void extends (b: B) => void)。简单说,函数参数的子类型关系与原类型"反向"

典型场景:函数参数的逆变

ts 复制代码
// 接收父类型 Animal 的函数(仅依赖 name)
const feedAnimal = (animal: Animal) => console.log(`喂 ${animal.name}`);
// 接收子类型 Dog 的函数类型
type FeedDog = (dog: Dog) => void;

// 逆变:feedAnimal 可赋值给 FeedDog
const feedDog: FeedDog = feedAnimal; // 合法
  • 安全性:feedDog 接收 Dog 类型参数,而 feedAnimal 仅需 name 属性,Dog 必然包含(安全)。
  • 反例:若允许接收 Dog 的函数赋值给接收 Animal 的函数,会因参数"能力不足"报错:
ts 复制代码
const badFeedDog = (dog: Dog) => dog.bark(); // 依赖 Dog 的 bark 方法
const badFeedAnimal: (a: Animal) => void = badFeedDog; // ❌ TS 报错(不安全)

四、双变:子类型关系的"双向兼容"

双变 指:当 BA 的子类型时,F<A>F<B> 互相兼容(既可以 F<B> extends F<A>,也可以 F<A> extends F<B>)。这种"双向宽松"的兼容性可能破坏类型安全,TypeScript 中仅在特定场景下存在。

典型场景:方法参数的双变(历史遗留)

早期 TypeScript 中,对象方法的参数是双变的(函数参数是逆变,但方法参数例外):

ts 复制代码
interface Handler {
  handle: (a: Animal) => void;
}
interface DogHandler {
  handle: (d: Dog) => void;
}

// 双变:Handler 与 DogHandler 互相兼容(TS 2.6 前默认行为)
const handler: Handler = { handle: (a) => {} };
const dogHandler: DogHandler = handler; // 合法(类似逆变)
const handler2: Handler = dogHandler; // 合法(类似协变,实际不安全)
  • 风险:handler2.handle 可能接收非 Dog 类型(如猫),但 dogHandler.handle 可能依赖 bark 方法,导致错误。
  • 现状:通过 strictFunctionTypes: true(严格模式)可禁用双变,使方法参数遵循逆变(推荐开启)。

五、不变:子类型关系的"完全一致"

不变 指:仅当 AB 完全相同时,F<A>F<B> 才兼容(既不协变也不逆变)。这是最严格的兼容性规则,避免协变/逆变可能的安全隐患。

典型场景:普通泛型的不变

ts 复制代码
// 泛型容器(默认不变)
type Box<T> = { value: T };

type AnimalBox = Box<Animal>;
type DogBox = Box<Dog>;

const dogBox: DogBox = { value: dog };
const animalBox: AnimalBox = dogBox; // ❌ 错误:Box<Dog> 与 Box<Animal> 互不兼容
  • 原因:若允许 DogBox 赋值给 AnimalBox,可能向 animalBox 写入非 Dog 类型(如猫),破坏 dogBox.value 的类型一致性。

六、实际中的一些场景

1. 事件处理(依赖逆变保证安全)

ts 复制代码
type Event = { type: string };
type MouseEvent = Event & { x: number; y: number };

// 处理通用事件的函数(可用于鼠标事件回调)
const logEvent = (e: Event) => console.log(e.type);
const handleClick: (e: MouseEvent) => void = logEvent; // 合法(逆变)

2. 状态管理(泛型不变避免类型污染)

ts 复制代码
// 状态容器(不变泛型,确保状态类型严格一致)
type StateContainer<T> = {
  state: T;
  setState: (newState: T) => void;
};

type UserState = { name: string };
type AdminState = { name: string; role: string };

// 错误:AdminState 是 UserState 的子类型,但容器不变
const userContainer: StateContainer<UserState> = {
  state: { name: "用户" },
  setState: (s) => {},
};
const adminContainer: StateContainer<AdminState> = userContainer; // ❌ 错误

3. 开启严格模式(禁用双变,增强安全性)

tsconfig.json 中开启 strictFunctionTypes: true,强制方法参数遵循逆变:

ts 复制代码
{
  "compilerOptions": {
    "strictFunctionTypes": true
  }
}

七、对比

概念 子类型关系(B extends A 时) 典型场景 安全性
协变 F<B> extends F<A> 数组、函数返回值 读取安全,写入需谨慎
逆变 F<A> extends F<B> 函数参数 输入安全
双变 F<A> extends F<B>F<B> extends F<A> 方法参数(非严格模式) 安全性低,可能隐患
不变 F<A> extends F<B> 当且仅当 A = B 普通泛型容器 最安全,限制严格

函数类型的兼容性,随参数类型变化而逆变的方式来判定,而返回值类型使用协变的方式确定;而数组类型兼容性与元素类型是协变

相关推荐
遇到困难睡大觉哈哈12 小时前
Harmonny os——《从 TypeScript 到 ArkTS 的适配规则》精简笔记
笔记·typescript·harmonyos·鸿蒙
by__csdn12 小时前
Vue 中计算属性、监听属性与函数方法的区别详解
前端·javascript·vue.js·typescript·vue·css3·html5
u***27611 天前
TypeScript 与后端开发Node.js
javascript·typescript·node.js
用户600071819101 天前
【翻译】TypeScript中可区分联合类型的省略
typescript
月弦笙音2 天前
【Promise.withResolvers】发现这个api还挺有用
前端·javascript·typescript
4***14902 天前
TypeScript在React中的前端框架
react.js·typescript·前端框架
槁***耿3 天前
TypeScript类型推断
前端·javascript·typescript
y***54883 天前
TypeScript在React项目中的状态管理
javascript·react.js·typescript
y***86693 天前
TypeScript在Electron应用中的使用
javascript·typescript·electron
初学者,亦行者3 天前
DevUI微前端集成实战解析
前端·typescript