为什么TypeScript只在被检查的类型是裸类型参数时,才会将条件类型分发到联合类型上?

前端开发 2026-07-12

我在努力理解当一个类型参数是“裸类型参数”时,条件类型的行为为何会不同。比如:

type Wrapped<T> = [T] extends [string] ? "is string" : "not string";
type Naked<T> = T extends string ? "is string" : "not string";

type Test1 = Wrapped<string | number>; // 'not string'
type Test2 = Naked<string | number>;   // 'is string' | 'not string'

我理解发生了什么(是分布式还是非分布式),但我并没有完全理解 为什么 TypeScript在内部会把它们这样区别对待。
这个区分背后的类型系统或编译器逻辑是什么?它又是如何影响通用库设计或运行时类型推断模式的?

解决方案

唯一一个我能客观验证的、关于“为什么 分布式条件类型 只在被检查的类型是裸类型参数时才会发生”的解释,是它作为条件类型特性的一部分被有意这样实现,如在 microsoft/TypeScript#21316 中所述。相关提交 只是说到了“会发生”这一点,而不是“为什么会这样”。我还没找到任何来源能解释为何要作出这样的选择。在那次提交之前,似乎条件类型在它被检查的类型上总是对联合类型进行分布,即使被检查的类型不是一个类型参数。

也就是说,问题中的 Wrapped<T> 示例在 T 中不会对联合类型进行分布,即使在另一个平行宇宙里条件类型总是可分布的情况下也一样Wrapped<string | number> 会展开为 [string | number] extends [string] ? "is string" : "not string"。类型 [string | number] 不是一个联合类型。它是一个一-元组,其元素类型是一个联合,但那不是同一回事。类型 [string | number][string] | [number] 也不同。因此不会发生分布,而由于 [string | number] 不能赋值给 [string],在平行宇宙和实际宇宙中你都会得到 "not string"

因此,即便在那个平行宇宙里条件类型总是对联合分布,你也可以通过将检查的源和目标都包裹在一个一元组中来关闭分布,如在 How to avoid distributive conditional types 中所述。是的,[T] 不再是裸类型参数,但“不裸”并不是事情不分布的主要原因。主要原因是 [T] 根本就不是一个联合类型,无论 T 是什么。


那么如果你的 Wrapped<T> 示例没有显示分布性与非分布性条件类型之间的差异,那还能看出什么呢?你需要做出一个让被检查的表达式不是裸类型参数但仍然是一个联合类型的例子。像这样:

type Inlined = (string | number) extends string ? "is string" : "not string";
// type Inlined = "not string"

在这里,Inlined 看起来像是 Naked<string | number> 的一个简单内联版本。但虽然 Naked<string | number> 的结果是 "is string" | "not string"Inlined 的结果却只是 "not string"。那是因为被检查的类型 string | number 不是一个类型参数,裸的也好非裸的也好。因此尽管 string | number 是一个联合类型,但并不会发生分布。在那个平行宇宙里,条件类型总是对联合分布时,Inlined 就会和 Naked<string | number> 相同。(有点遗憾的是它并非如此,因为这样的行为更易于推理和解释。)

或者,为了用一个不是裸的类型参数来演示,你也可以看

type Clothed<T> = keyof T extends "a" ? "is a" : "not a";
type Test3 = Clothed<{ a: 1, b: 1 }>;
// type Test3 = "not a"

在这里,被检查的类型 Clothed<T>keyof T,它涉及 一个类型参数,但并非只是一个类型参数。它被 keyof 运算符“穿戴”着。尽管 keyof T 很可能是一个联合(例如 keyof {a: 1, b: 1}"a" | "b"),但条件类型并不具备分布性。你得到的只是 "not a" 而不是 "is a" | "not a"。如果你想要那个联合,你得绕点弯路,确保被检查的类型是一个裸类型参数,例如

type _N<T> = T extends "a" ? "is a" : "not a";
type Naked2<T> = _N<keyof T>
type Test4 = Naked2<{ a: 1, b: 1 }>
// type Test4 = "not a" | "is a"

type Naked3<T> = keyof T extends infer K ? K extends "a" ? "is a" : "not a" : never;
type Test5 = Naked3<{ a: 1, b: 1 }>
// type Test5 = "not a" | "is a"

在这两种情况下,都会引入一个新的类型参数,并将 keyof T 作为它的类型参数。新引入的类型参数随后在条件类型中直接被检查,因此具备分布性。

Playground链接到代码

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章