条件类型
本教程共 80 篇 · 第 43 篇 · 更新于 2026-08-10 · 约 12 分钟阅读
本节目标:掌握
T extends U ? X : Y的语法和语义,理解分布条件类型的行为,学会用never做类型过滤,并为下一章映射类型与条件类型的组合打好基础。
上一章你已经用到了条件类型——Exclude 和 Extract 的核心实现就是一行条件类型。这一章我们系统地把这个语法吃透。
条件类型是 TypeScript 类型系统中的”if/else”——它让你根据类型的结构关系做分支判断,在类型层面实现逻辑。
基本语法:T extends U ? X : Y
语法跟 JavaScript 的三元表达式一模一样,只是把值换成了类型:
type IsString<T> = T extends string ? "是字符串" : "不是字符串";
type A = IsString<"hello">; // "是字符串"
type B = IsString<42>; // "不是字符串"
type C = IsString<string>; // "是字符串"
T extends U 的意思是”T 是否可以赋值给 U”。如果可以,走 ? 后面的分支;如果不可以,走 : 后面的分支。
光这么用没什么意思
问题来了:上面这种写法,你自己看一眼就知道结果,干嘛用条件类型?条件类型的威力在于和泛型组合——当 T 还没确定的时候,条件类型才能发挥”动态判断”的能力:
type Flatten<T> = T extends any[] ? T[number] : T;
type Str = Flatten<string[]>; // string(从数组里提取元素类型)
type Num = Flatten<number>; // number(不是数组,原样返回)
type Bool = Flatten<boolean[]>; // boolean
Flatten<T> 等价于:如果 T 是数组,返回它的元素类型;否则原样返回。这里的 T[number] 是索引访问类型——它取数组的元素类型。string[] 的 string[] [number] 就是 string。
你不需要提前知道 T 是什么,条件类型让类型系统在”调用处”根据传入的实际类型做判断。
取代函数重载的真实场景
条件类型可以大幅简化需要用重载做到的逻辑。假设你要根据传入类型的不同返回不同标签:
不用条件类型你得这样:
interface IdLabel { id: number }
interface NameLabel { name: string }
// 三个重载!每加一种新类型,重载数量翻倍
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "未实现";
}
用条件类型可以精简到一个函数:
type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel;
function createLabel<T extends number | string>(input: T): NameOrId<T> {
throw "未实现";
}
const a = createLabel("typescript"); // NameLabel
const b = createLabel(100); // IdLabel
分布条件类型:对联合类型的”映射”
前面 Exclude 的实现你已经看到了——T extends U ? never : T。当 T 是联合类型时,它会把条件分配到联合的每个成员上。这就是分布条件类型(distributive conditional types)。
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// Result = string[] | number[]
发生了什么?ToArray<string | number> 被展开为:
ToArray<string> | ToArray<number>
→ (string extends any ? string[] : never) | (number extends any ? number[] : never)
→ string[] | number[]
这个行为对联合类型来说几乎总是你想要的——你要的正是”把联合里每种类型分别处理,然后把结果合并”。
为什么分布条件总是跟泛型搭配
注意一个关键细节:条件类型只有在作用于裸类型参数(naked type parameter)时才会分布。
// ✅ 会分布——T 是裸类型参数
type Distributive<T> = T extends any ? T[] : never;
type D = Distributive<string | number>; // string[] | number[]
// ❌ 不会分布——T 被包在了数组里
type NonDistributive<T> = T[] extends any[] ? T[] : never;
type ND = NonDistributive<string | number>; // (string | number)[]
第二个例子中,T[] 不是裸类型参数——条件类型先计算 T[](结果是 (string | number)[]),再判断 (string | number)[] extends any[],不会触发分布。
如何禁用分布
如果你不想让条件类型分布(比如你要的就是 (string | number)[] 而不是 string[] | number[]),用方括号包裹类型参数:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result = ToArrayNonDist<string | number>;
// Result = (string | number)[] —— 不分布
[T] extends [any] 把 T 包在元组里,破坏了裸类型参数的条件,分布行为就不会触发。
never 过滤技巧
联合类型中 never 会被自动吸收——这意味着你可以用 never 来”去除”不想要的类型。这就是 Exclude 的原理:
type Exclude<T, U> = T extends U ? never : T;
但这个技巧不只限于 Exclude。你可以在任何条件类型中用 never 做过滤:
// 只保留字符串字面量类型
type StringOnly<T> = T extends string ? T : never;
type Filtered = StringOnly<"a" | "b" | 1 | 2>;
// "a" | "b" —— 1 和 2 被过滤掉了
在后面的映射类型中,你还会看到这个技巧——它可以在遍历对象类型的时候”跳过”不满足条件的属性。
条件类型约束:在 true 分支里收窄类型
条件类型的 true 分支不仅返回结果,还会**收窄(narrow)**泛型参数的类型。这意味着在 true 分支内部,TypeScript 知道 T 满足 extends 后面的条件:
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
sender: string;
}
interface Notification {
message: number;
level: string;
}
type A = MessageOf<Email>; // string
type B = MessageOf<Notification>; // number
type C = MessageOf<{ x: number }>; // never(没有 message 属性)
在 ? 后面的分支(true 分支),TypeScript 知道 T 一定有 message 属性,所以 T["message"] 是合法的。
如果不用条件类型,你得这样写:
// ❌ 报错:T 上不存在 message
// type MessageOf<T> = T["message"];
// ✅ 但必须约束 T——不灵活
type MessageOf2<T extends { message: unknown }> = T["message"];
第二种写法的问题在于:T 被”永久约束”了——你不能对没有 message 属性的类型使用。条件类型把约束变成了”按需判断”:有就取,没有就返回 never。
条件类型 + infer:预告
条件类型最强大的部分其实是 infer——上一章 ReturnType 和 Parameters 里已经用到了。这里做一个预告,为后续章节铺路。
infer 让你在条件类型的模式匹配中”捕捉”类型片段:
// 不用 infer:用 T[number] 手动提取数组元素
type Flatten1<T> = T extends any[] ? T[number] : T;
// 用 infer:让 TypeScript 推断数组元素类型
type Flatten2<T> = T extends Array<infer Item> ? Item : T;
当你在 extends 后面用了 infer Item,TypeScript 会自动推断那个位置的具体类型并绑定到 Item。对于复杂的类型模式(比如提取 Promise 内层类型、函数的参数元组等),infer 比手动拆解直观得多。
下一章讲映射类型时,你会看到条件类型 + 映射类型如何组合出强大的类型变换能力——比如”把一个类型的所有函数属性挑出来”或者”把所有属性名转成 getter 方法名”。
小结
条件类型是 TypeScript 类型系统的”运行时判断”——只是这个”运行时”发生在编译期。三个核心要点:
- 语法:
T extends U ? X : Y,跟 JavaScript 三元表达式同款。 - 分布:作用于裸类型参数时,自动分配给联合类型的每个成员。
- 过滤:利用
never在联合中会被吸收的特性,实现”剔除不想要的类型”。
加上 infer,条件类型就成了 TypeScript 类型体操的核心舞台。不过在实际开发中,你不需要写太多复杂的条件类型——前面两章的内置工具类型已经覆盖了绝大部分日常需求。