WinddSnow

TypeScript-Generics-Part2-Conditional-Types-Infer-Distributive-Mapped-Types

字数统计: 2.5k阅读时长: 10 min
2026/07/31

第70课:泛型(下)——条件类型、infer、分布式条件类型、映射类型

上一节课我们学习了泛型的基础:类型参数如何让函数和接口变得通用且类型安全。本节课将进入 TypeScript 泛型的高级领域——条件类型(Conditional Types)允许你根据类型关系作出“类型层面的判断”;**infer 关键字让你在条件类型中提取和推断类型的局部信息;分布式条件类型是条件类型作用于联合类型时的自动分配行为;映射类型**(Mapped Types)则能遍历类型的属性并逐个修改,是 TypeScript 内置工具类型的底层引擎。这四种能力组合在一起,构成了 TypeScript 类型编程的“图灵完备”基础,用于实现 PickOmitReturnType 等高级工具类型。


1. 条件类型:T extends U ? X : Y

条件类型的语法类似于 JavaScript 的三元运算符,但它是作用于类型层面的。它根据 T 是否可赋值给 U 来决定最终的类型。

1
2
3
4
type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>; // true
type B = IsString<42>; // false

条件类型本身并不立即计算——当传入具体类型参数时,TypeScript 才根据条件选择分支。

1.1 条件类型的常用模式

条件类型广泛应用于类型守卫和工具类型中:

1
2
3
4
5
// 过滤出特定类型
type FilterNumbers<T> = T extends number ? T : never;

type Mixed = string | number | boolean;
type NumbersOnly = FilterNumbers<Mixed>; // number(联合类型会分配,见下文)

1.2 条件类型与泛型结合

条件类型通常与泛型一起使用,构成“类型层面的函数”——输入类型,输出新类型。


2. infer:在条件类型中推断类型

infer 关键字只能在条件类型的 extends 子句中使用,用于声明一个待推断的类型变量。当条件成立时,这个变量会被推断为具体的类型,并可在 true 分支中使用。

1
2
3
4
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = (x: number) => string;
type Result = ReturnType<Fn>; // string

infer R 告诉 TypeScript:“如果 T 是一个函数类型,请推断出它的返回值类型,并赋给 R”。你可以把 infer 看作类型层面的“解构赋值”。

2.1 提取数组元素类型

1
2
3
4
5
type ElementType<T> = T extends (infer U)[] ? U : T;

type Item1 = ElementType<string[]>; // string
type Item2 = ElementType<number[]>; // number
type Item3 = ElementType<boolean>; // boolean(非数组,返回原类型)

2.2 提取 Promise 包裹的值

1
2
3
4
type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T;

type P1 = Awaited<Promise<string>>; // string
type P2 = Awaited<Promise<Promise<number>>>; // number(递归展开)

2.3 提取函数第一个参数

1
2
3
type FirstParameter<T> = T extends (first: infer P, ...rest: any[]) => any ? P : never;

type Param1 = FirstParameter<(a: number, b: string) => void>; // number

infer 的核心价值:它允许你“窥探”一个复杂类型的内部结构,提取出某个组成部分,从而在类型层面实现模式匹配。


3. 分布式条件类型(Distributive Conditional Types)

当条件类型作用于裸类型参数(即未被包裹在数组、元组、对象等结构中的泛型),且该参数是联合类型时,条件类型会自动分布到联合类型的每个成员上,然后将结果重新联合。

1
2
3
4
5
type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;
// 等价于 (string extends any ? string[] : never) | (number extends any ? number[] : never)
// 结果:string[] | number[]

分布式条件类型的条件

  • T 必须是裸类型参数(直接写在 extends 前面)。
  • 传入的实参是联合类型

3.1 禁用分布行为

如果不想让条件类型分布式地作用于联合类型,可以用元组包裹

1
2
3
4
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type Result = ToArrayNonDist<string | number>;
// 结果:(string | number)[] —— 不分布

3.2 利用分布进行过滤:实现 ExcludeExtract

TypeScript 内置的 ExcludeExtract 就是利用分布式条件类型实现的:

1
2
3
4
5
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;

type Excluded = MyExclude<'a' | 'b' | 'c', 'a' | 'b'>; // 'c'
type Extracted = MyExtract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'

工作过程(以 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 内置工具类型(PartialReadonlyPick 等)的底层引擎。

4.1 基本语法

1
2
3
4
5
6
7
8
9
10
11
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

interface User {
name: string;
age: number;
}

type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; }
  • keyof T 获取 T 的所有键的联合类型(如 'name' | 'age')。
  • [P in keyof T] 遍历这些键,P 为每次迭代的当前键。
  • T[P] 索引访问类型,获取属性值的类型。

4.2 修饰符:添加或移除 readonly?

使用 +- 来显式添加或移除修饰符。+ 通常可省略。

1
2
3
4
5
6
7
8
9
10
11
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};

type Partial<T> = {
[P in keyof T]?: T[P];
};

type Required<T> = {
[P in keyof T]-?: T[P];
};

4.3 键的重映射(Key Remapping)

通过 as 子句,可以在迭代过程中重新映射键名(TypeScript 4.1+)。

1
2
3
4
5
6
7
8
9
10
11
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

interface Person {
name: string;
age: number;
}

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

Capitalize<string & P> 将首字母大写,string & P 是为了确保 P 被当作字符串字面量处理。

4.4 条件映射:过滤出特定类型

结合条件类型,可以在映射过程中过滤掉某些键(将其映射为 nevernever 的属性会被自动移除)。

1
2
3
4
5
6
7
8
9
10
11
12
type PickByValueType<T, U> = {
[P in keyof T as T[P] extends U ? P : never]: T[P];
};

interface Mixed {
name: string;
age: number;
isAdmin: boolean;
}

type StringProps = PickByValueType<Mixed, string>;
// { name: string; }

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 中排除 nullundefined
  • **ReturnType<T>**:获取函数返回值类型。
  • **Parameters<T>**:获取函数参数类型元组。

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) type Result = (string | number) extends string ? true : false 的结果是什么?
    A. true
    B. false
    C. boolean
    D. true | false

  2. (单选) infer 关键字的作用是什么?
    A. 推断泛型的默认值。
    B. 在条件类型中声明并推断一个类型变量。
    C. 将类型转换为接口。
    D. 定义映射类型。

  3. (填空) 要提取函数 fn 的参数类型元组,应使用内置工具类型 ______<typeof fn>

  4. (多选) 以下哪些是映射类型可以实现的功能?
    A. 将接口所有属性变为只读
    B. 将接口所有属性变为可选
    C. 选取接口的特定属性子集
    D. 修改属性名(通过键重映射)

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要实现两个 TypeScript 工具类型:

  • DeepReadonly<T>:递归地将对象类型的所有属性(包括嵌套对象)设为只读。数组类型递归应用于元素类型。
  • DeepPartial<T>:递归地将对象类型的所有属性设为可选。数组类型递归应用于元素类型。
    类型应当处理基本类型(stringnumber 等)时直接返回该类型,不进行递归。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成这两个工具类型的 TypeScript 代码。提示词中需明确指定递归终止条件、对数组和对象的处理方式,并提供使用示例验证深层嵌套属性的只读/可选效果。


课后练习答案

一、概念自测答案

  1. B

    • 解析:条件 (string | number) extends string 中,string | number 是联合类型,但不是裸类型参数,因此不会分布式计算。直接判断整个联合类型是否可赋值给 string,显然不是,因此为 false
  2. B

    • 解析:infer 用于在条件类型的 extends 子句中声明一个待推断的类型变量,并在 true 分支中使用。
  3. Parameters

    • 解析:Parameters<T> 提取函数类型的参数元组。
  4. 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 是对象,使用映射类型遍历属性,属性值递归调用 DeepReadonlyDeepPartial
  • 注意保留属性修饰符(如原始只读属性在 DeepPartial 后应保持只读)。
  • 提供至少一个包含嵌套对象和数组的测试接口,展示完整类型转换效果。输出完整代码。”
CATALOG
  1. 1. 第70课:泛型(下)——条件类型、infer、分布式条件类型、映射类型
    1. 1.1. 1. 条件类型:T extends U ? X : Y
      1. 1.1.1. 1.1 条件类型的常用模式
      2. 1.1.2. 1.2 条件类型与泛型结合
    2. 1.2. 2. infer:在条件类型中推断类型
      1. 1.2.1. 2.1 提取数组元素类型
      2. 1.2.2. 2.2 提取 Promise 包裹的值
      3. 1.2.3. 2.3 提取函数第一个参数
    3. 1.3. 3. 分布式条件类型(Distributive Conditional Types)
      1. 1.3.1. 3.1 禁用分布行为
      2. 1.3.2. 3.2 利用分布进行过滤:实现 Exclude 和 Extract
    4. 1.4. 4. 映射类型(Mapped Types):遍历并修改属性
      1. 1.4.1. 4.1 基本语法
      2. 1.4.2. 4.2 修饰符:添加或移除 readonly 和 ?
      3. 1.4.3. 4.3 键的重映射(Key Remapping)
      4. 1.4.4. 4.4 条件映射:过滤出特定类型
    5. 1.5. 5. 综合应用:解析常见内置工具类型
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)