不是只有服务能分布,类型也能分布:解密 TypeScript 分布式条件类型

172 阅读10分钟

1. 什么是条件类型?

条件类型的基本语法是:

T extends U ? X : Y

可以理解成 JS 里的三元运算符,但这是类型层面:

  • 如果类型 T 兼容 U,就使用类型 X

  • 否则使用类型 Y

如下代码示例所示

type IsString<T> = T extends string ? true : false;

type A = IsString<"abc">; // true
type B = IsString<123>; // false

20250604094623

2. 什么是分布式条件类型?

分布式条件类型是 TypeScript 中的一种特殊行为,当条件类型作用于泛型类型参数时,如果该类型是 联合类型(A | B),则条件会 分布 到每一个联合成员上,分别计算,再将结果合并成一个新的联合类型。

例子:

type ToFlag<T> = T extends number ? "num" : "other";

type R = ToFlag<number | string | boolean>;
// 分布后:
// ToFlag<number> | ToFlag<string> | ToFlag<boolean>
// => 'num' | 'other' | 'other'
// => 'num' | 'other'

20250604094739

3. 什么是“裸类型参数”?

在 TypeScript 的类型系统里,我们经常会写一些条件类型,比如这样:

type IsString<T> = T extends string ? true : false;

它的意思是,如果类型 Tstring,就返回 true,否则返回 false。当我们传入一个单一类型,比如:

type A = IsString<"hello">; // true
type B = IsString<123>; // false

这时候非常直接:因为 'hello' 确实是字符串,结果就是 true;123 不是字符串,就是 false

但如果我们传入一个联合类型,比如:

type C = IsString<"hello" | 123>;

我们可能以为会返回 false,但 TypeScript 实际上返回的是:

type C = true | false;

20250604095101

这是因为 TypeScript 的条件类型具有一个特殊的行为——分布式(distributive)特性。当我们传入的泛型类型是联合类型,比如 'hello' | 123,而条件类型的写法又是 T extends U ? X : Y 这种形式时,TypeScript 会自动对联合类型的每个成员分别进行判断。也就是说:

IsString<'hello' | 123>
=> IsString<'hello'> | IsString<123>
=> true | false
=> boolean

这个行为只有在 T 是裸类型参数时才会发生。那么什么是裸类型参数呢?

所谓裸类型参数,就是指这个类型参数 T 被直接写在 extends 的左侧,没有被任何结构包裹住。像我们刚才写的:

T extends string ? true : false

这里 T 是裸的,它没有被放进数组、对象或别的东西里,所以它会被 TypeScript 自动地“分发”到联合类型的每一个成员上。

但如果我们改变一下写法,比如包裹在数组里,像这样:

type IsStringStrict<T> = [T] extends [string] ? true : false;

这时候,再传入 'hello' | 123

type D = IsStringStrict<"hello" | 123>; // 结果是 false

20250604095159

注意,不是 true | false,而是直接 false。为什么?因为我们把 T 放到了一个元组 [T] 中,它就不再是裸的,TypeScript 也就不会再进行分布。它会把整个 'hello' | 123 当作一个整体,问这个整体是否是 string,当然答案是 false

可以理解成这样:

  • T 是裸的,TypeScript 会遍历联合类型的每一项单独处理。

  • T 是被包住的,TypeScript 会把整个联合类型当作一个单独的块整体处理。

这个行为让我们可以灵活控制是否要对联合类型的成员分别处理。如果我们想处理联合类型的每个成员,比如提取其中是字符串的那一项,我们就写裸参数的条件类型:

type FilterString<T> = T extends string ? T : never;

type R = FilterString<string | number | boolean>;
// => string

如果我们不想触发分布,而是要判断整个类型是否是一个纯字符串类型的组合,可以包一下:

type StrictString<T> = [T] extends [string] ? T : never;

type A = StrictString<string>; // string
type B = StrictString<string | number>; // never

我们可以把 extends 的左边想象成一个判断的“镜头”:

  • 如果直接是 T,就像是给每一个成员拍独照,会得到很多张照片(分布);

  • 如果是 [T]{ foo: T },那就是把他们拉进了一个合照框,只能得到一张照片(整体比较)。

总结起来就是一句话:

条件类型中,如果 T 是裸类型参数,并且 T 是联合类型,TypeScript 会将其拆分每个成员进行判断;一旦我们用结构包裹了 T,它就失去了这种分布能力。

这就是裸类型参数在 TypeScript 条件类型中触发“分布式”行为的全部机制。是否要触发分布,完全取决于我们是否希望对联合类型逐项处理,还是作为整体判断。懂得这个之后,我们就能自由掌控复杂的类型变换了。

4. 为什么 TypeScript 要设计成“会分布”?

TypeScript 支持条件类型的分布式行为,其核心目的是为了方便我们在类型层面上对联合类型的每个成员进行独立映射。这种能力极大地提升了类型系统的表达力和灵活性,尤其是在处理复杂泛型变换时非常有用。

举个典型的例子,假设我们想把联合类型中的每个成员都包裹成一个数组类型。我们可以这样写:

type WrapArray<T> = T extends any ? T[] : never;

当传入 string | number 时:

type A = WrapArray<string | number>; // 得到 string[] | number[]

20250604100156

这个结果是怎么来的?因为 T 是裸类型参数,而 T extends any 会触发分布,TypeScript 实际上是这样处理的:

WrapArray<string> | WrapArray<number>
=> string[] | number[]

也就是说,每一个联合成员都被独立套了一层数组,再合并回一个新的联合类型。

如果 TypeScript 的条件类型没有分布式的特性,那么上面这种类型映射就无法实现。我们只能拿到整个联合类型 string | number,没办法分别处理 stringnumber,结果就会变成 (string | number)[],而不是我们想要的 string[] | number[]

所以说,分布式条件类型并不是一个“副作用”,而是一种设计上的能力,它为 TypeScript 类型系统带来了强大的“类型映射编程”能力。

5. 分布式 vs 非分布式:详细对比

条件写法是否分布行为说明
T extends U ? X : Y✅ 分布联合类型的每个成员单独判断并合并结果
[T] extends [U] ? X : Y❌ 不分布整体作为一个类型参与判断,只产生一个结果

示例对比:

type D1<T> = T extends string ? 1 : 0;
type D2<T> = [T] extends [string] ? 1 : 0;

type R1 = D1<"a" | 123>; // => D1<"a"> | D1<123> => 1 | 0 ✅ 分布发生
type R2 = D2<"a" | 123>; // => [ "a" | 123 ] extends [string] => false => 0 ❌ 不分布

在上面的代码中有如下解释:

  • D1T 是裸类型参数,传入 "a" | 123 后,类型被自动拆解为 "a"123 分别判断是否是 string,因此结果是 1 | 0

  • D2T 被包裹在了元组 [T] 中,不再是裸参数,条件不再对联合成员分发判断,而是整体比较 [ "a" | 123 ] 是否可以赋值给 [string],由于 123 不满足,所以结果为 false,返回 0

6. 分布式条件类型的实战应用

6.1 从联合类型中提取指定类型(如:只保留 string

type OnlyString<T> = T extends string ? T : never;

type R = OnlyString<string | number | boolean>;
// => string

在上面的代码中,由于 T 是裸类型参数,条件类型对联合类型中的每个成员分别判断,只有 string 满足 extends string 条件,其它变为 never,最终结果合并为 string

6.2 从联合类型中排除指定类型(如:去掉 null

type RemoveNull<T> = T extends null ? never : T;

type R = RemoveNull<string | null | number>;
// => string | number

在上面的代码中,条件判断为 true 时返回 never,相当于“删除”这一分支,剩下的类型被保留下来。

这些正是 TypeScript 内置工具类型的底层实现机制:

  • Extract<T, U> 等价于:T extends U ? T : never

  • Exclude<T, U> 等价于:T extends U ? never : T

例如:

type A = Extract<string | number | boolean, string>; // string
type B = Exclude<string | null | number, null>; // string | number

通过分布式条件类型,TypeScript 实现了这些类型级别的“过滤器”,让我们可以灵活处理联合类型中的每个成员。

7. 高阶技巧:联合类型的变换与类型推断

7.1 将联合类型成员转换为数组类型

type WrapArray<T> = T extends any ? T[] : never;

type R = WrapArray<string | number>;
// => string[] | number[]

在上面的代码中,T 是裸类型参数且为联合类型,触发分布式条件类型 WrapArray<string> | WrapArray<number> → string[] | number[]。如果没有分布,结果将是 (string | number)[],不是我们想要的结构。

7.2 提取联合类型每个对象的键名

type AllKeys<T> = T extends any ? keyof T : never;

type R = AllKeys<{ a: 1 } | { b: 2 }>;
// => 'a' | 'b'

在上面的代码中,通过分布式条件类型,我们分别对 { a: 1 }{ b: 2 } 应用 keyof,再合并结果。 而直接写 keyof ({ a: 1 } | { b: 2 }) 得到的是 never,因为联合对象的公共键为空。

7.3 配合 infer 进行返回值推断(支持分布)

type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;

type A = ReturnTypeOf<() => string>; // string

type B = ReturnTypeOf<(() => string) | (() => number)>;
// => ReturnTypeOf<() => string> | ReturnTypeOf<() => number>
// => string | number ✅ 分布 + 推断同时发生

20250604101517

在上面的代码中分布式条件类型不仅能触发每个成员的判断,还支持 infer 推断变量 R。因此多个函数类型组成的联合会被分别推断返回值,最后合并为联合返回类型。

这些技巧构成了 TypeScript 类型系统的“类型变换引擎”,让我们可以像操作值一样精细控制类型结构。我们还可以基于这些模式构造更强大的工具类型,比如映射函数参数、提取类字段、组合交叉类型等。

8. 小结与记忆口诀

当我们使用条件类型时,如果 T 是一个联合类型,并且它在 extends 左边是裸参数(也就是说没有被任何结构包裹),TypeScript 就会自动对联合类型的每一个成员进行逐一判断,然后将结果合并为一个新的联合类型。这就是所谓的分布式条件类型(Distributive Conditional Types)。

我们只需要记住一句口诀:

“裸参数 + 联合类型 → 条件会自动分布”

这个行为虽然看起来很特殊,但实际上它是 TypeScript 类型系统设计中非常强大的能力之一,可以让我们对联合类型进行灵活、精准的映射与过滤。

判断一个条件类型是否会触发分布,我们只需要从以下几个维度来看:

判断条件说明是否触发分布
T 是联合类型?比如 string | number
T 是裸参数?直接写在 extends 左侧
使用 T extends ... ? ...是标准的条件类型结构
T 被结构包裹了吗?[T]{ x: T }Partial<T>❌ 不分布

当上述三个“是”和最后一个“否”同时成立时,就可以确定这个条件类型是分布式的。

换句话说,只要我们在条件类型中让 T 保持“裸露”,联合类型就会被拆解并单独处理。如果我们不希望它分布,最简单的方法就是用结构(如 [T])把它包裹起来,TypeScript 就会将整个联合类型视为一个整体来判断。

掌握这个机制后,我们就可以自由地在类型层对联合体进行筛选、变换、包装、推断——这也是很多高级类型工具(如 ExtractExcludeReturnType)背后真正的原理核心。

总结

分布式条件类型是 TypeScript 中的一种机制,当泛型参数是联合类型并以裸形式出现在 extends 条件中时,类型会自动对联合的每个成员分别进行判断并合并结果。这个特性让我们可以轻松实现类型级的过滤、映射和转换。只要 T 是裸参数且是联合类型,条件类型就会“分布”处理每个分支;如果你不希望这种行为,只需将 T 包裹进结构中(如 [T])。它是构建 ExcludeExtract 等工具类型的基础,也是类型体操的核心能力之一。理解并掌握分布式条件类型,是进阶 TypeScript 类型编程的关键一步。