跳到主要内容

分布式条件类型 (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>

从类型中排除 nullundefined

// 内置实现
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] 判断
常见应用ExcludeExtractNonNullable都基于分布式条件类型

面试常见问题

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 在联合类型中被自动消除,剩下的就是排除后的结果。