第70课:泛型(下)——条件类型、infer、分布式条件类型、映射类型
上一节课我们学习了泛型的基础:类型参数如何让函数和接口变得通用且类型安全。本节课将进入 TypeScript 泛型的高级领域——条件类型(Conditional Types)允许你根据类型关系作出“类型层面的判断”;**infer 关键字让你在条件类型中提取和推断类型的局部信息;分布式条件类型是条件类型作用于联合类型时的自动分配行为;映射类型**(Mapped Types)则能遍历类型的属性并逐个修改,是 TypeScript 内置工具类型的底层引擎。这四种能力组合在一起,构成了 TypeScript 类型编程的“图灵完备”基础,用于实现 Pick、Omit、ReturnType 等高级工具类型。
1. 条件类型:T extends U ? X : Y
条件类型的语法类似于 JavaScript 的三元运算符,但它是作用于类型层面的。它根据 T 是否可赋值给 U 来决定最终的类型。
1 | type IsString<T> = T extends string ? true : false; |
条件类型本身并不立即计算——当传入具体类型参数时,TypeScript 才根据条件选择分支。
1.1 条件类型的常用模式
条件类型广泛应用于类型守卫和工具类型中:
1 | // 过滤出特定类型 |
1.2 条件类型与泛型结合
条件类型通常与泛型一起使用,构成“类型层面的函数”——输入类型,输出新类型。
2. infer:在条件类型中推断类型
infer 关键字只能在条件类型的 extends 子句中使用,用于声明一个待推断的类型变量。当条件成立时,这个变量会被推断为具体的类型,并可在 true 分支中使用。
1 | type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; |
infer R 告诉 TypeScript:“如果 T 是一个函数类型,请推断出它的返回值类型,并赋给 R”。你可以把 infer 看作类型层面的“解构赋值”。
2.1 提取数组元素类型
1 | type ElementType<T> = T extends (infer U)[] ? U : T; |
2.2 提取 Promise 包裹的值
1 | type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T; |
2.3 提取函数第一个参数
1 | type FirstParameter<T> = T extends (first: infer P, ...rest: any[]) => any ? P : never; |
infer 的核心价值:它允许你“窥探”一个复杂类型的内部结构,提取出某个组成部分,从而在类型层面实现模式匹配。
3. 分布式条件类型(Distributive Conditional Types)
当条件类型作用于裸类型参数(即未被包裹在数组、元组、对象等结构中的泛型),且该参数是联合类型时,条件类型会自动分布到联合类型的每个成员上,然后将结果重新联合。
1 | type ToArray<T> = T extends any ? T[] : never; |
分布式条件类型的条件:
T必须是裸类型参数(直接写在extends前面)。- 传入的实参是联合类型。
3.1 禁用分布行为
如果不想让条件类型分布式地作用于联合类型,可以用元组包裹:
1 | type ToArrayNonDist<T> = [T] extends [any] ? T[] : never; |
3.2 利用分布进行过滤:实现 Exclude 和 Extract
TypeScript 内置的 Exclude 和 Extract 就是利用分布式条件类型实现的:
1 | type MyExclude<T, U> = T extends U ? never : T; |
工作过程(以 Exclude 为例):T 是 'a' | 'b' | 'c',分布到每个成员——'a' extends U ? never : 'a' → never,'b' → never,'c' → 'c';结果 never | never | 'c' = 'c'。
4. 映射类型(Mapped Types):遍历并修改属性
映射类型通过 [P in Keys]: Type 语法,遍历一组键(Keys)并为每个键生成一个属性。它是 TypeScript 内置工具类型(Partial、Readonly、Pick 等)的底层引擎。
4.1 基本语法
1 | type Readonly<T> = { |
keyof T获取T的所有键的联合类型(如'name' | 'age')。[P in keyof T]遍历这些键,P为每次迭代的当前键。T[P]索引访问类型,获取属性值的类型。
4.2 修饰符:添加或移除 readonly 和 ?
使用 + 或 - 来显式添加或移除修饰符。+ 通常可省略。
1 | type Mutable<T> = { |
4.3 键的重映射(Key Remapping)
通过 as 子句,可以在迭代过程中重新映射键名(TypeScript 4.1+)。
1 | type Getters<T> = { |
Capitalize<string & P> 将首字母大写,string & P 是为了确保 P 被当作字符串字面量处理。
4.4 条件映射:过滤出特定类型
结合条件类型,可以在映射过程中过滤掉某些键(将其映射为 never,never 的属性会被自动移除)。
1 | type PickByValueType<T, U> = { |
5. 综合应用:解析常见内置工具类型
以下工具类型的实现都依赖于映射类型与条件类型。
- **
Partial<T>**:全部属性变为可选。 - **
Required<T>**:全部属性变为必填。 - **
Readonly<T>**:全部属性变为只读。 - **
Pick<T, K>**:从T中选取属性子集。 - **
Omit<T, K>**:从T中删除属性子集(Pick<T, Exclude<keyof T, K>>)。 - **
Record<K, T>**:构造键为K、值为T的对象类型。 - **
Exclude<T, U>**:从联合类型T中排除可赋值给U的成员。 - **
Extract<T, U>**:从联合类型T中提取可赋值给U的成员。 - **
NonNullable<T>**:从T中排除null和undefined。 - **
ReturnType<T>**:获取函数返回值类型。 - **
Parameters<T>**:获取函数参数类型元组。
课后练习
一、概念自测(选择题 / 填空题)
(单选)
type Result = (string | number) extends string ? true : false的结果是什么?
A.true
B.false
C.boolean
D.true | false(单选)
infer关键字的作用是什么?
A. 推断泛型的默认值。
B. 在条件类型中声明并推断一个类型变量。
C. 将类型转换为接口。
D. 定义映射类型。(填空) 要提取函数
fn的参数类型元组,应使用内置工具类型______<typeof fn>。(多选) 以下哪些是映射类型可以实现的功能?
A. 将接口所有属性变为只读
B. 将接口所有属性变为可选
C. 选取接口的特定属性子集
D. 修改属性名(通过键重映射)
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要实现两个 TypeScript 工具类型:
DeepReadonly<T>:递归地将对象类型的所有属性(包括嵌套对象)设为只读。数组类型递归应用于元素类型。DeepPartial<T>:递归地将对象类型的所有属性设为可选。数组类型递归应用于元素类型。
类型应当处理基本类型(string、number等)时直接返回该类型,不进行递归。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成这两个工具类型的 TypeScript 代码。提示词中需明确指定递归终止条件、对数组和对象的处理方式,并提供使用示例验证深层嵌套属性的只读/可选效果。
课后练习答案
一、概念自测答案
B
- 解析:条件
(string | number) extends string中,string | number是联合类型,但不是裸类型参数,因此不会分布式计算。直接判断整个联合类型是否可赋值给string,显然不是,因此为false。
- 解析:条件
B
- 解析:
infer用于在条件类型的extends子句中声明一个待推断的类型变量,并在true分支中使用。
- 解析:
Parameters- 解析:
Parameters<T>提取函数类型的参数元组。
- 解析:
A、B、C、D
- 解析:映射类型可通过
readonly修饰符实现 A,通过?实现 B,通过as重映射和Pick实现 C 和 D。
- 解析:映射类型可通过
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请用 TypeScript 实现两个递归工具类型DeepReadonly<T>和DeepPartial<T>。要求:
- 若
T是基本类型(string | number | boolean | null | undefined | Function等),直接返回T。- 若
T是数组/元组类型,递归应用于每个元素:DeepReadonly<T[number]>[]或对应的映射形式。- 若
T是对象,使用映射类型遍历属性,属性值递归调用DeepReadonly或DeepPartial。- 注意保留属性修饰符(如原始只读属性在
DeepPartial后应保持只读)。- 提供至少一个包含嵌套对象和数组的测试接口,展示完整类型转换效果。输出完整代码。”