文章目录

TypeScript分布式条件类型在联合类型上的展开行为

发布于 2026-05-01 14:17:22 · 浏览 12 次 · 评论 0 条

TypeScript分布式条件类型在联合类型上的展开行为

TypeScript 中的条件类型在处理联合类型时,存在一种被称为“分布式条件类型”的特殊机制。理解这一机制对于编写复杂的泛型工具类型至关重要。以下是关于这一展开行为的实操指南。


1. 理解自动分发机制

当一个条件类型作用于一个类型参数 T,且 T 是一个联合类型时,TypeScript 会自动将该条件类型分发到联合类型的每个成员上。

定义一个简单的条件类型 ToArray,它将传入的类型转换为数组类型:

type ToArray = T extends any ? T[] : never;

定义一个联合类型 InputTypes

type InputTypes = string | number;

使用 ToArray 处理该联合类型:

type Result = ToArray<InputTypes>;

观察 Result 的类型。你会发现它不是 (string | number)[],而是:

type Result = string[] | number[];

这证明了条件类型自动“遍历”了 string | number 中的每一项,分别进行了 T extends any 的判断,最后将结果重新组合成了联合类型。


2. 识别触发条件

分布式条件类型并不是无条件发生的,它必须严格满足以下“裸类型参数”规则。

检查条件类型的 extends 左侧是否直接引用了泛型参数 T

以下情况会触发分发:

// 直接使用 T,触发分发
type Distributed = T extends U ? X : Y;

以下情况不会触发分发:

// T 被包裹在元组中,不触发分发
type NotDistributed1 = [T] extends [U] ? X : Y;

// T 被作为某个类型的属性,不触发分发
type NotDistributed2 = { x: T } extends { x: U } ? X : Y;

记住核心原则:只有当类型参数 T 裸露地出现在 extends 关键字的左侧时,分发才会发生。


3. 可视化分发流程

为了更直观地理解 TypeScript 在编译时如何处理这一过程,我们可以通过以下流程描述来追踪其内部逻辑。

假设输入类型为 A | B | C,条件类型为 T extends U ? X : Y

graph LR Start["输入: T = A | B | C"] --> Check{检查 T 是否为裸类型参数} Check -- 是 --> Distribute["开始分发"] Check -- 否 --> Single["作为整体处理"] Distribute --> Item1["分支 1: A extends U ? X : Y"] Distribute --> Item2["分支 2: B extends U ? X : Y"] Distribute --> Item3["分支 3: C extends U ? X : Y"] Item1 --> Res1["结果类型 R1"] Item2 --> Res2["结果类型 R2"] Item3 --> Res3["结果类型 R3"] Res1 --> Final["最终输出: R1 | R2 | R3"] Res2 --> Final Res3 --> Final Single --> DirectResult["最终输出: (A|B|C) extends U ? X : Y"]

分析上图逻辑:系统识别到联合类型后,将其拆解为独立分支,每个分支单独进行条件判断,最后通过联合运算符 | 将所有分支的结果合并。


4. 掌握展开的数学规律

我们可以用形式化的方式描述这一展开规则。假设 $T = A \cup B$(即 $T$ 是 $A$ 和 $B$ 的联合类型),那么:

$$ (A \cup B) \text{ extends } U ? X : Y \iff (A \text{ extends } U ? X : Y) \cup (B \text{ extends } U ? X : Y) $$

这意味着,分发过程本质上是“分配律”在类型系统中的应用。

验证这一规律。编写以下代码:

type Message = string | number;

// 分发逻辑:string extends 'hello' ? true : false | number extends 'hello' ? true : false
// 结果:false | false => false
type Test1 = Message extends 'hello' ? true : false; // 结果为 false

// 分发逻辑:string extends string ? true : false | number extends string ? true : false
// 结果:true | false => true | false
type Test2 = Message extends string ? true : false; // 结果为 true | false

查看 Test2 的类型定义,它确实是 true | false。因为 string 匹配了 string,而 number 没有匹配,两者结果取并集。


5. 阻止自动分发

在实际开发中,有时我们希望将联合类型视为一个整体,而不是拆开处理。这可以通过将类型参数包装在数组或元组中来实现。

编写一个不进行分发的工具类型 ToNonDistributedArray

type ToNonDistributedArray = [T] extends [any] ? T[] : never;

应用该类型到联合类型上:

type Result2 = ToNonDistributedArray<string | number>;

检查 Result2 的类型。此时结果是 (string | number)[]

对比这两种方式的区别:

特性 分布式条件类型 非分布式条件类型
语法 T extends U [T] extends [U]
输入示例 string \vert number string \vert number
处理方式 拆分成员分别判断 视为整体判断
输出示例 string[] \vert number[] (string \vert number)[]

6. 实战应用:过滤联合类型

利用分发机制,我们可以创建一个“过滤器”,从联合类型中剔除不符合条件的类型。

编写一个 NonNullable 工具类型,用于剔除 nullundefined

type MyNonNullable = T extends null | undefined ? never : T;

定义一个包含 null 的联合类型:

type Data = string | number | null | undefined;

应用 MyNonNullable

type CleanData = MyNonNullable<Data>;

分析推导过程:

  1. string extends null | undefined ? 否 -> 保留 string
  2. number extends null | undefined ? 否 -> 保留 number
  3. null extends null | undefined ? 是 -> 返回 never
  4. undefined extends null | undefined ? 是 -> 返回 never

观察 CleanData 的最终结果:

type CleanData = string | number;

由于 never 在联合类型中会被吸收($A \cup \text{never} = A$),所以 nullundefined 被成功移除。


7. 进阶应用:提取函数重载的返回类型

TypeScript 标准库中的 ReturnType 就是利用分布式条件类型实现的。当传入联合类型的函数时,它会返回联合类型的返回值。

编写两个不同类型的函数:

function fn1(x: string): number {
  return x.length;
}

function fn2(x: number): string {
  return x.toString();
}

组合它们的类型:

type FnUnion = typeof fn1 | typeof fn2;

使用内置的 ReturnType(或手动实现版本):

// 内部实现类似:type ReturnType = T extends (...args: any) => infer R ? R : any;
type ReturnUnion = ReturnType<FnUnion>;

检查 ReturnUnion 的结果。根据分发规则:

  1. typeof fn1 被推断为返回 number
  2. typeof fn2 被推断为返回 string
  3. 最终结果为 number | string

这种机制使得处理重载函数或函数联合类型变得非常安全且类型推导准确。


8. 在映射类型中使用分发

在映射类型中,如果值位置使用了条件类型,分发机制依然会生效。

编写一个映射类型,将对象中所有的 string 类型属性变为大写形式(模拟),其他属性保持原样:

type StringifyProps = {
  [K in keyof T]: T[K] extends string ? Uppercase : T[K]
};

定义一个对象类型:

type User = {
  id: number;
  name: string;
  email: string;
  isAdmin: boolean;
};

应用映射类型:

type ProcessedUser = StringifyProps<User>;

查看结果:

type ProcessedUser = {
  id: number;
  name: "UPPERCASE";
  email: "UPPERCASE";
  isAdmin: boolean;
};

注意:这里 T[K] 是一个类型参数(属性值的类型)。当它恰好是联合类型(例如 string | number)时,分发机制同样会运行,分别判断成员是否满足 extends string


9. 总结检查清单

在编写复杂的泛型逻辑时,按照以下步骤检查你的代码:

  1. 确认 T 是否需要被分发。
  2. 如果需要分发,确保 T 直接出现在 extends 左侧。
  3. 如果不需要分发,使用 [T] extends [any] 或类似的包装技巧进行规避。
  4. 利用 never 在联合类型中的吸收特性来过滤不需要的类型成员。

评论 (0)

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

扫一扫,手机查看

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