在 TypeScript 类型系统中,类型兼容性是核心支柱,而协变 、逆变 、双变 和不变 则是描述类型转换规则的四大概念。它们决定了不同类型的集合(如数组、函数、泛型)之间如何安全地兼容,直接影响代码的类型安全性。但是之前一直很难理解协变与逆变,所以写本文记录一下。
一、类型兼容性
首先明确子类型 与父类型 的判定
子类型
TypeScript 中,子类型的核心判定依据是"赋值兼容性" :若类型 B 的所有可能值都能安全地赋值给类型 A 的变量,则 B 是 A 的子类型(记为 B extends A)。通俗说,子类型的范围是父类型范围的"子集" 。
不同类型的子类型关系示例:
- 对象类型 :结构兼容即子类型
若B包含A所有属性且类型匹配,则B是A的子类型(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 的子集
- 联合类型 :范围收窄即子类型
若B的所有成员都属于A的成员,则B是A的子类型(B范围更小)。
ts
type NumOrStr = number | string; // 父类型:范围 {number, string}
type OnlyNum = number; // 子类型:范围 {number}(NumOrStr 的子集)
const num: OnlyNum = 123;
const numOrStr: NumOrStr = num; // 合法:number 是 number|string 的子集
- 基础类型 :字面量是基础类型的子类型
字面量类型的范围是基础类型的子集(如123是number的子集)。
ts
type Age = number;
type SpecificAge = 18; // 子类型:范围仅 {18}
const age: Age = 18;
const specificAge: SpecificAge = 18;
const age2: Age = specificAge; // 合法:18 是 number 的子集
协变与逆变这两个术语源于数学中的范畴论(Category Theory) ,后来被计算机科学引入类型系统领域。物理学中也有类似的,简单来说就是"一个量随另一个量的变化,如果一致就是协变,否则就是逆变"(不太严谨),在类型系统中就是某个类型集合兼容性随某些类型变化是怎么样的?比如数组类型随其元素类型变化后的类型兼容性如何变化?函数类型随其参数类型或返回值类型变化后的类型兼容性如何?
二、协变:子类型关系的"正向传递"
协变 指:当 B 是 A 的子类型时,B 的"容器类型"也是 A 的"容器类型"的子类型(即 F<B> extends F<A>)。简单说,容器的子类型关系与原类型"同向" 。
典型场景:
- 数组的协变
若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 未禁止,历史遗留问题)。
- 安全性:从
- 函数返回值的协变
若函数返回B(子类型),则该函数是返回A(父类型)的函数的子类型。
ts
const getDog = (): Dog => ({ name: "旺财", bark: () => {} });
type GetAnimal = () => Animal;
const getAnimal: GetAnimal = getDog; // 合法:返回 Dog 的函数是返回 Animal 的函数的子类型
-
- 安全性:调用
getAnimal时,返回值至少包含Animal所需的name(安全)。
- 安全性:调用
三、逆变:子类型关系的"反向传递"
逆变 指:当 B 是 A 的子类型时,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 报错(不安全)
四、双变:子类型关系的"双向兼容"
双变 指:当 B 是 A 的子类型时,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(严格模式)可禁用双变,使方法参数遵循逆变(推荐开启)。
五、不变:子类型关系的"完全一致"
不变 指:仅当 A 与 B 完全相同时,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 |
普通泛型容器 | 最安全,限制严格 |
函数类型的兼容性,随参数类型变化而逆变的方式来判定,而返回值类型使用协变的方式确定;而数组类型兼容性与元素类型是协变