分布式条件类型 (Distributive Conditional Types)
在 TypeScript 的类型体操中,分布式条件类型是一个非常重要但容易让人困惑的特性。理解它,能帮助你看懂和编写各种高级工具类型。
什么是条件类型?
在学习"分布式"之前,先回顾一下**条件类型(Conditional Types)**的基本语法:
type Result = T extends U ? X : Y;
它的含义是:如果类型 T 可以赋值给类型 U,则结果为 X,否则为 Y。就像 JavaScript 中的三元表达式,但操作的是类型而不是值。
type IsString<T> = T extends string ? "是字符串" : "不是字符串";
type A = IsString<string>; // "是字符串"
type B = IsString<number>; // "不是字符串"
分布式条件类型的定义
当条件类型作用于泛型参数,且该泛型参数是一个联合类型(Union Type)时,条件类型会自动拆开联合类型,逐个进行判断,最后再合并结果。这就是所谓的"分布式"行为。
官方定义:
当条件类型中被检查的类型(
extends左边)是一个**裸类型参数(naked type parameter)**时,该条件类型对联合类型具有分布性。
来看一个例子:
type IsString<T> = T extends string ? "yes" : "no";
// 传入联合类型
type Result = IsString<string | number | boolean>;
你可能以为结果是 "no"(因为 string | number | boolean 整体不是 string),但实际结果是:
// 分布式展开过程:
// IsString<string> | IsString<number> | IsString<boolean>
// = "yes" | "no" | "no"
// = "yes" | "no"
type Result = "yes" | "no";
TypeScript 把联合类型拆开,对每个成员分别应用条件类型,最后把结果合并成新的联合类型。
分布式行为的触发条件
分布式行为不是任何条件类型都会触发的,它需要同时满足两个条件:
条件一:被检查的类型必须是泛型参数
// ✅ T 是泛型参数 → 触发分布
type Example1<T> = T extends string ? "yes" : "no";
type R1 = Example1<string | number>; // "yes" | "no"
// ❌ 直接写具体类型 → 不触发分布
type R2 = string | number extends string ? "yes" : "no"; // "no"
条件二:泛型参数必须是"裸"的
所谓"裸类型参数",就是泛型参数没有被包裹在任何其他类型结构中:
// ✅ 裸类型参数 → 触发分布
type Naked<T> = T extends string ? "yes" : "no";
type R1 = Naked<string | number>; // "yes" | "no"
// ❌ 被包裹在元组中 → 不触发分布
type Wrapped<T> = [T] extends [string] ? "yes" : "no";
type R2 = Wrapped<string | number>; // "no"(整体判断)
常见的"包裹"方式:
[T] // 元组包裹
T[] // 数组(注意:这里 T 仍然是裸的,因为 T 在 extends 左边)
Promise<T> // 泛型包裹
{ value: T } // 对象包裹
裸 = 分布,穿衣 = 不分布。 只要给泛型参数"穿上"任何外层类型,分布式行为就会被阻止。
实际应用:内置工具类型
TypeScript 的很多内置工具类型都依赖分布式条件类型。理解了分布式行为,这些工具类型的实现就一目了然了。
Exclude<T, U>
从联合类型 T 中排除可以赋值给 U 的成员:
// 内置实现
type Exclude<T, U> = T extends U ? never : T;
// 使用示例
type T1 = Exclude<"a" | "b" | "c", "a">;
// 展开过程:
// ("a" extends "a" ? never : "a") | ("b" extends "a" ? never : "b") | ("c" extends "a" ? never : "c")
// = never | "b" | "c"
// = "b" | "c"
关键点:never 在联合类型中会被自动移除,这是 Exclude 能工作的原因。
Extract<T, U>
从联合类型 T 中提取可以赋值给 U 的成员(与 Exclude 相反):
// 内置实现
type Extract<T, U> = T extends U ? T : never;
type T2 = Extract<string | number | boolean, string | number>;
// = string | number
NonNullable<T>
从类型中排除 null 和 undefined:
// 内置实现
type NonNullable<T> = T extends null | undefined ? never : T;
type T3 = NonNullable<string | null | undefined>;
// = string
实战练习
练习 1:提取函数类型
从联合类型中提取所有函数类型:
type ExtractFunctions<T> = T extends (...args: any[]) => any ? T : never;
type Mixed = string | (() => void) | number | ((x: number) => string);
type OnlyFunctions = ExtractFunctions<Mixed>;
// = (() => void) | ((x: number) => string)
练习 2:获取联合类型中的字符串字面量
type StringOnly<T> = T extends string ? T : never;
type Values = 1 | "hello" | true | "world" | 42;
type Strings = StringOnly<Values>;
// = "hello" | "world"
练习 3:将联合类型的每个成员包装成数组
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// = string[] | number[]
// 注意:不是 (string | number)[]
如何阻止分布式行为?
有时候你不希望联合类型被拆开,而是想把它作为一个整体来判断。方法就是给泛型参数"穿衣服"——用 [T] 包裹:
// 分布式版本
type ToArray<T> = T extends any ? T[] : never;
type R1 = ToArray<string | number>;
// = string[] | number[]
// 非分布式版本
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type R2 = ToArrayNonDist<string | number>;
// = (string | number)[]
一个更实际的例子——判断一个类型是否恰好是 never:
// ❌ 错误写法:永远不会返回 "是 never"
type IsNever1<T> = T extends never ? "是 never" : "不是 never";
type R1 = IsNever1<never>; // never(不是 "是 never"!)
// ✅ 正确写法:阻止分布
type IsNever2<T> = [T] extends [never] ? "是 never" : "不是 never";
type R2 = IsNever2<never>; // "是 never"
type R3 = IsNever2<string>; // "不是 never"
为什么错误写法不行?因为 never 是空联合类型(没有任何成员),分布式条件类型对空联合类型的结果就是 never——根本不会进入任何分支。
never 是空联合类型。分布式条件类型遍历联合类型的每个成员,而 never 没有成员可遍历,所以结果直接是 never。这是一个常见的坑!
进阶:分布式条件类型与 infer
分布式条件类型经常和 infer 关键字配合使用,实现更强大的类型推断:
// 提取 Promise 的内部类型(支持联合类型)
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type R1 = UnpackPromise<Promise<string> | Promise<number> | boolean>;
// 展开:string | number | boolean
// 提取函数返回值类型(支持联合类型)
type ReturnTypes<T> = T extends (...args: any[]) => infer R ? R : never;
type Fns = (() => string) | (() => number);
type Returns = ReturnTypes<Fns>;
// = string | number
总结速查表
| 概念 | 说明 | 示例 |
|---|---|---|
| 条件类型 | T extends U ? X : Y | 类型层面的三元表达式 |
| 分布式触发 | 裸泛型参数 + 联合类型 | T extends string ? ... |
| 阻止分布 | 用 [T] 包裹 | [T] extends [string] ? ... |
never 的坑 | never 是空联合,分布结果为 never | 用 [T] extends [never] 判断 |
| 常见应用 | Exclude、Extract、NonNullable | 都基于分布式条件类型 |
面试常见问题
Q1:什么是分布式条件类型?
当条件类型中 extends 左边是一个裸泛型参数时,如果传入联合类型,TypeScript 会将联合类型拆开,对每个成员分别应用条件类型,最后将结果合并为新的联合类型。
Q2:如何阻止分布式行为?
将泛型参数用元组包裹:[T] extends [U] ? X : Y。这样 T 不再是"裸"的,联合类型会作为整体进行判断。
Q3:为什么 T extends never ? "yes" : "no" 在 T = never 时结果是 never 而不是 "yes"?
因为 never 是空联合类型(零个成员的联合)。分布式条件类型会遍历联合类型的每个成员分别判断,但 never 没有成员可遍历,所以结果直接是 never。要正确判断 never,需要用 [T] extends [never] 阻止分布。
Q4:Exclude<T, U> 是如何工作的?
Exclude<T, U> = T extends U ? never : T。利用分布式行为,联合类型的每个成员分别与 U 比较,匹配的返回 never(被移除),不匹配的保留。最终 never 在联合类型中被自动消除,剩下的就是排除后的结果。