TypeScript 条件类型:T extends U ? X : Y
TypeScript 的条件类型提供了一种根据类型关系动态选择结果类型的机制。其基本语法为 T extends U ? X : Y,含义是:如果类型 T 可以赋值给类型 U(即 T 是 U 的子类型),则整个表达式的结果类型为 X;否则为 Y。这种能力在构建泛型工具类型、实现类型推导和约束时极为强大。
理解基本行为
创建一个最简单的条件类型示例:
type IsString<T> = T extends string ? true : false;
- 当你使用
IsString<"hello">时,由于"hello"是string的字面量子类型,结果为true。 - 当你使用
IsString<number>时,number不能赋值给string,结果为false。
注意:这里的 extends 不是指类继承,而是指“可赋值性”(assignability)。只要 T 的值能安全地赋给 U 类型的变量,就满足 T extends U。
分布式条件类型
当条件类型的左侧(即 T)是一个泛型类型参数,且该参数被传入一个联合类型(如 string | number)时,TypeScript 会自动将条件类型“分发”到联合类型的每个成员上。
例如:
type ToPrimitive<T> = T extends string
? string
: T extends number
? number
: T extends boolean
? boolean
: never;
type Test = ToPrimitive<string | number | boolean | Date>;
// 结果为 string | number | boolean
执行过程如下:
- 将
string | number | boolean | Date拆分为四个独立类型。 - 对每个类型分别应用
ToPrimitive:string→stringnumber→numberboolean→booleanDate→never
- 合并结果:
string | number | boolean | never,而never在联合类型中会被自动忽略,最终得到string | number | boolean。
要禁用分布行为,可将泛型参数包裹在元组中:
type NotDistributive<T> = [T] extends [string] ? true : false;
type A = NotDistributive<string | number>; // false(整体判断,非分发)
实用场景:提取函数返回类型
利用条件类型配合 infer 关键字,可以提取复杂类型中的信息。例如,获取函数的返回类型:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
- 如果
T是一个函数类型,则通过infer R捕获其返回类型R。 - 否则返回
never。
使用:
function add(a: number, b: number): number {
return a + b;
}
type AddResult = ReturnType<typeof add>; // number
构建过滤工具类型
编写一个从联合类型中剔除某些类型的工具:
type Exclude<T, U> = T extends U ? never : T;
- 对
T中的每个成员,若其可赋值给U,则替换为never;否则保留。 - 联合类型中的
never会被自动剔除。
调用:
type T0 = Exclude<'a' | 'b' | 'c', 'a' | 'c'>; // 'b'
type T1 = Exclude<string | number | (() => void), Function>; // string | number
TypeScript 内置的 Exclude 工具类型正是基于此原理。
相反,提取属于某类的类型:
type Extract<T, U> = T extends U ? T : never;
示例:
type T2 = Extract<'a' | 'b' | 1 | 2, string>; // 'a' | 'b'
处理嵌套结构:递归条件类型
条件类型支持递归,可用于处理嵌套对象或数组。
定义一个将所有属性转为只读的深层工具类型:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends Record<string, unknown>
? DeepReadonly<T[K]>
: T[K];
};
- 遍历
T的每个键K。 - 如果
T[K]是一个对象(通过Record<string, unknown>判断),则递归应用DeepReadonly。 - 否则保持原类型。
使用:
interface User {
name: string;
address: {
city: string;
zip: number;
};
}
type ReadonlyUser = DeepReadonly<User>;
// name: string(只读)
// address: { readonly city: string; readonly zip: number; }(深层只读)
常见陷阱与注意事项
-
空对象类型陷阱
Record<string, unknown>匹配几乎所有对象,但不包括null和undefined。若需兼容,应使用更宽松的判断:type IsObject<T> = T extends object ? (T extends null ? never : T) : never; -
any 与 unknown 的行为差异
any extends string ? X : Y会同时走X和Y分支(因为any既是任何类型的子类型,又可以接受任何类型),最终结果为X | Y。unknown extends string ? X : Y结果为Y,因为unknown不能赋值给string。
-
避免无限递归
在递归条件类型中,确保有明确的终止条件。例如,对原始类型(string,number,boolean等)不再递归。
高级技巧:结合映射类型与条件类型
组合映射类型和条件类型,可实现按需转换属性。
例如,仅将可选属性变为必选,其余不变:
type RequiredIfOptional<T> = {
[K in keyof T]-?: T[K] extends Required<T>[K] ? T[K] : T[K];
};
更实用的例子:将对象中所有函数属性的返回类型提取出来:
type FunctionReturnTypes<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => infer R ? R : never;
}[keyof T];
解释:
- 先构造一个新对象,每个属性值是原函数的返回类型(非函数则为
never)。 - 然后通过
[keyof T]索引访问,得到所有返回类型的联合。
测试:
interface API {
getUser(): User;
save(data: string): boolean;
version: string;
}
type Returns = FunctionReturnTypes<API>; // User | boolean
内置工具类型的实现参考
TypeScript 官方提供的许多工具类型都依赖条件类型。以下是部分实现逻辑:
| 工具类型 | 简化实现 |
|---|---|
Exclude<T, U> |
T extends U ? never : T |
Extract<T, U> |
T extends U ? T : never |
NonNullable<T> |
T extends null \| undefined ? never : T |
Parameters<T> |
T extends (...args: infer P) => any ? P : never |
ConstructorParameters<T> |
T extends new (...args: infer P) => any ? P : never |
这些类型展示了条件类型与 infer 的典型结合方式。
掌握 T extends U ? X : Y 的核心在于理解“可赋值性”和“分布行为”。通过组合 infer、映射类型和递归,你可以构建出高度灵活的类型逻辑,让 TypeScript 在编译期为你完成复杂的类型推导和验证。

暂无评论,快来抢沙发吧!