在TypeScript的类型系统里,联合类型并不是一个简单的集合容器,而是一种会被条件类型特殊处理的语法结构。当我们试图对string | number | boolean这样的联合类型中的每个成员统一施加某种变换时,普通的映射类型无法直接生效,因为映射类型只作用于对象类型的键。要实现递归遍历与变换,必须借助分布式条件类型与递归类型定义的组合。

分布式条件类型如何触发联合成员遍历
TypeScript中有一个重要特性:当条件类型T extends U ? X : Y中的T是一个泛型参数,并且传入的是联合类型时,条件类型会针对联合中的每一个成员分别求值,最终将结果重新组合成联合类型。这一行为被称为分布式条件类型。理解这一点是处理联合类型遍历的基础,因为这意味着我们不需要手动展开联合,编译器会自动帮我们逐个处理成员。
例如,我们有一个类型ToString<T>,定义为T extends any ? string : never。当我们传入ToString<number | boolean>时,编译器实际上会计算number extends any ? string : never得到string,再计算boolean extends any ? string : never得到string,最后合并为string | string,也就是string。虽然这个例子看起来平淡无奇,但它揭示了联合类型在条件类型中被自动拆分的机制。
如果我们要做更有意义的变换,比如给每个成员加上前缀,就可以结合模板字面量类型与infer。不过对于非字符串的原始类型,需要先做一层判断。下面这段代码展示了如何利用分布式条件类型将联合中的字符串成员添加msg_前缀,而非字符串成员保持原样:
type AddPrefix<T> = T extends string
? `msg_${T}`
: T;
type Result = AddPrefix<'error' | 'warn' | number>;
// Result 类型为 'msg_error' | 'msg_warn' | number
这里要注意,分布式行为只在T是裸泛型参数时生效。如果写成[T] extends [any]这种用元组包裹的形式,就会关闭分发,导致整个联合被当作一个整体判断。因此在设计联合遍历类型时,必须谨慎使用方括号,以免意外禁用分发。
结合infer与递归实现深层变换
仅仅做一层前缀添加往往不能满足复杂场景。当联合类型中的成员本身是对象类型或数组类型,并且我们希望递归地变换其内部字段时,就需要引入递归类型。递归类型在TypeScript中通过类型别名自我引用实现,但必须设置明确的终止条件,否则编译器会报出“类型实例化过深”的错误。
假设我们有联合类型{ id: number } | { name: string },希望把每个对象中的所有属性值都包装成可空类型。我们可以定义一个递归映射类型,并在条件类型中判断当前成员是否为对象。如果是对象,则对其属性做映射;如果不是,则直接包裹。示例如下:p
type DeepNullable<T> = T extends object
? { [K in keyof T]: DeepNullable<T[K]> | null }
: T | null;
type Input = { id: number } | { name: string; tag: boolean };
type Output = DeepNullable<Input>;
// Output 类型为
// ({ id: number | null } | null) | ({ name: string | null; tag: boolean | null } | null)
在上面的代码中,DeepNullable通过T extends object判断是否为对象,利用keyof与映射类型深入每个属性,并对属性值再次调用自身。由于原始类型的分支直接返回T | null不再递归,因此满足了终止条件。对于联合类型Input,分布式条件类型先将其拆为两个对象分支,各自递归完成后再组合。
如果联合中混合了原始类型与对象类型,上述写法同样适用,因为T extends object对number等原始类型会走 false 分支,直接返回T | null。这种写法在处理API响应联合、状态机事件联合时非常实用,能够保证每个成员的结构被一致地改造。
避免无限递归与类型实例化过深的问题
在类型体操中,递归是一把双刃剑。TypeScript编译器对类型实例化的深度有内部限制,一旦超过就会抛出错误。特别是在联合类型变换中,如果每次递归都产生新的联合分支,深度会呈指数增长。我们需要通过精确的终止条件和必要时使用元组截断来控制规模。
一种常见策略是引入深度计数参数。例如定义一个Depth泛型,默认从某个数字字面量类型开始,每次递归递减,当到达0时直接返回原类型。虽然TypeScript没有原生的数字递减运算,但我们可以用元组长度来模拟。以下示例展示带深度限制的联合变换:
type Prev = [never, 0, 1, 2, 3, 4, 5];
type MapWithDepth<T, D extends number = 5> = D extends 0
? T
: T extends string
? `m_${T}`
: T extends object
? { [K in keyof T]: MapWithDepth<T[K], Prev[D]> }
: T;
type Demo = MapWithDepth<{ a: 'x' | 'y' } | string>;
// 安全地在深度5内完成变换
这里Prev元组充当了减法表,Prev[D]取到上一层深度。当D为0时直接返回T,避免继续展开。对于联合类型,分布式条件类型依旧生效,因此Demo中的string分支和对象分支会被分别处理且都受深度保护。
另一个实用技巧是尽量将联合的拆分放在最外层,把复杂递归放在单一成员内部,以减少编译器在联合维度上的重复展开。同时,在编辑器中用鼠标悬停查看中间类型,可以帮助定位是哪一层导致了深度爆炸。掌握这些细节后,递归遍历并变换联合类型所有成员就不再是无迹可寻的黑魔法,而是一套可拆解、可调试的明确模式。
TypeScript联合类型类型递归修改时间:2026-08-19 04:40:31