条件类型与 infer:从类型中提取子类型
第8章:条件类型与 infer:从类型中提取子类型
理解条件类型与 infer是掌握 TypeScript 类型系统的关键一步。
本章核心问题:如何在实际项目中正确使用从类型中提取子类型?关键的设计决策和陷阱是什么?
读完本章你将理解:
- 基本语法:
T extends U ? X : Y - 分布式条件类型:联合类型的自动展开
- 阻止分布:用
[T] extends [U]
Level 1 · 你需要知道的(1-3年经验)
基本语法:T extends U ? X : Y
条件类型的语法看起来像三元表达式,读起来也类似:"如果 T 能赋值给 U,则结果为 X,否则为 Y"。
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true,字符串字面量类型也满足 extends string
注意:这是类型层面的判断,不是运行时代码。extends 在条件类型中的含义是"可赋值性":如果类型 T 的任何值都可以赋值给类型 U,则条件成立。
// 嵌套条件类型:多分支判断
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends null ? "null" :
T extends undefined ? "undefined" :
"object";
type R1 = TypeName<string>; // "string"
type R2 = TypeName<42>; // "number"
type R3 = TypeName<boolean>; // "boolean"
type R4 = TypeName<string[]>; // "object"
分布式条件类型:联合类型的自动展开
当 T 是裸类型参数(不被任何包裹结构修饰),且被用在 T extends U ? X : Y 中时,TypeScript 会自动将联合类型的每个成员单独求值,然后将结果合并。
type IsString<T> = T extends string ? true : false;
// T 是联合类型时,条件类型分布到每个成员
type R = IsString<string | number | boolean>;
// 等价于:IsString<string> | IsString<number> | IsString<boolean>
// 等价于:true | false | false
// 化简为:boolean
这就是 Exclude 和 Extract 工作原理的基础:
// Exclude<T, U> 的工作原理
type Exclude<T, U> = T extends U ? never : T;
type E = Exclude<"a" | "b" | "c", "a" | "b">;
// "a" extends "a" | "b" ? never : "a" → never
// "b" extends "a" | "b" ? never : "b" → never
// "c" extends "a" | "b" ? never : "c" → "c"
// 结果:never | never | "c" = "c"
分布式触发条件:
T必须是裸类型参数(泛型直接参与判断)- 如果 T 被包裹(如
T[]、[T]、Promise<T>),就不会分布
阻止分布:用 [T] extends [U]
有时你不希望联合类型被分布展开,用元组包裹即可阻止:
// 分布式版本:联合类型被拆开求值
type IsStringDistributive<T> = T extends string ? true : false;
type D = IsStringDistributive<string | number>; // boolean (true | false)
// 非分布式版本:整个联合类型作为整体判断
type IsStringNonDistributive<T> = [T] extends [string] ? true : false;
type ND = IsStringNonDistributive<string | number>; // false(整体不满足 extends string)
// 实际用途:判断是否为 never
type IsNever<T> = [T] extends [never] ? true : false;
type N1 = IsNever<never>; // true
type N2 = IsNever<string>; // false
// 为什么不能用分布式?
type BrokenIsNever<T> = T extends never ? true : false;
type Broken = BrokenIsNever<never>; // never,不是 true!
// 原因:never 联合类型展开后没有成员,条件类型结果为 never
Level 2 · 它是怎么运行的(3-5年经验)
infer 关键字:在条件类型中提取类型
infer 是条件类型的核心能力,允许你在匹配模式时捕获某个位置的类型,并给它一个名字用于结果。
语法:在 extends 的右侧,将某个位置替换为 infer X,X 就是被捕获的类型。
提取函数返回类型
// 手动实现 ReturnType
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function greet(name: string): string {
return `Hello, ${name}`;
}
type GreetReturn = MyReturnType<typeof greet>; // string
// 非函数类型返回 never
type NotFn = MyReturnType<string>; // never
提取 Promise 的值类型
type UnpackPromise<T> = T extends Promise<infer V> ? V : T;
type A = UnpackPromise<Promise<string>>; // string
type B = UnpackPromise<Promise<number[]>>; // number[]
type C = UnpackPromise<string>; // string(不是 Promise,原样返回)
提取数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : never;
type N = ElementType<number[]>; // number
type S = ElementType<string[]>; // string
type U = ElementType<(string | number)[]>; // string | number
type X = ElementType<string>; // never(不是数组)
// 更通用的版本,同时处理 ReadonlyArray
type ElementType<T> = T extends readonly (infer E)[] ? E : never;
const arr = [1, 2, 3] as const;
type E = ElementType<typeof arr>; // 1 | 2 | 3(字面量类型联合)
提取函数参数类型
type FunctionArgs<T> = T extends (...args: infer P) => any ? P : never;
function login(username: string, password: string, remember: boolean): void {}
type Args = FunctionArgs<typeof login>;
// [username: string, password: string, remember: boolean]
// 提取第一个参数
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type First = FirstArg<typeof login>; // string
// 提取最后一个参数
type LastArg<T> = T extends (...args: infer P) => any
? P extends [...any[], infer L] ? L : never
: never;
type Last = LastArg<typeof login>; // boolean
逐步构建 Awaited<T>(递归条件类型)
TypeScript 4.5 内置了 Awaited<T>,但自己实现它能帮助理解递归条件类型:
// 版本 1:只处理一层 Promise
type Awaited1<T> = T extends Promise<infer V> ? V : T;
type A1 = Awaited1<Promise<string>>; // string
type A2 = Awaited1<Promise<Promise<string>>>; // Promise<string>(只解一层!)
// 版本 2:递归处理多层嵌套
type Awaited2<T> = T extends Promise<infer V> ? Awaited2<V> : T;
type A3 = Awaited2<Promise<string>>; // string
type A4 = Awaited2<Promise<Promise<string>>>; // string
type A5 = Awaited2<Promise<Promise<Promise<number>>>>; // number
// 版本 3:处理 thenable 对象(完整模拟标准库实现)
type Awaited3<T> =
T extends null | undefined ? T :
T extends object & { then(onfulfilled: infer F, ...args: any[]): any } ?
F extends (value: infer V, ...args: any[]) => any ?
Awaited3<V> :
never :
T;
递归类型的注意事项:
// TypeScript 编译器对递归深度有限制
// 过深的递归会导致 Type instantiation is excessively deep 错误
// 简单函数组合,递归限制较宽松
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// 处理 JSON 类型的递归定义
type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| { [key: string]: JsonValue };
never 在条件类型中的作用:过滤联合成员
never 是联合类型中的"零元素"——任何联合类型加上 never 等于原来的联合类型。利用这个性质可以精确过滤联合类型:
// 过滤掉字符串中不满足某个条件的成员
type FilterString<T, U extends string> = T extends U ? T : never;
type Colors = "red" | "green" | "blue" | "yellow";
type WarmColors = FilterString<Colors, "red" | "yellow">; // "red" | "yellow"
// 实用工具:只保留对象中值为特定类型的键
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
interface User {
id: number;
name: string;
email: string;
age: number;
active: boolean;
}
type StringKeys = KeysOfType<User, string>; // "name" | "email"
type NumberKeys = KeysOfType<User, number>; // "id" | "age"
拆解 KeysOfType 的工作过程:
// 第一步:映射类型,值类型满足则保留键名,否则替换为 never
{
[K in keyof User]: User[K] extends string ? K : never
}
// 等价于:
// {
// id: never;
// name: "name";
// email: "email";
// age: never;
// active: never;
// }
// 第二步:[keyof User] 取所有值构成联合类型
// never | "name" | "email" | never | never
// = "name" | "email"
实用工具类型
UnpackPromise<T>
type UnpackPromise<T> = T extends Promise<infer V> ? V : T;
// 用于处理 async 函数返回值
async function fetchUser(id: number) {
return { id, name: "Alice", email: "[email protected]" };
}
type FetchUserResult = UnpackPromise<ReturnType<typeof fetchUser>>;
// { id: number; name: string; email: string }
// 等价于直接用内置的 Awaited:
type FetchUserResult2 = Awaited<ReturnType<typeof fetchUser>>;
ElementType<T>
type ElementType<T> = T extends readonly (infer E)[] ? E : never;
// 从数组类型提取元素,无需知道数组的具体类型
function processFirst<T extends readonly unknown[]>(arr: T): ElementType<T> {
return arr[0] as ElementType<T>;
}
const nums = [1, 2, 3] as const;
const first = processFirst(nums); // 类型:1(字面量类型!)
FunctionArgs<T> 在高阶函数中的应用
type FunctionArgs<T> = T extends (...args: infer P) => any ? P : never;
type FunctionReturn<T> = T extends (...args: any[]) => infer R ? R : never;
// 构建 memoize 函数,保留原函数的完整类型签名
function memoize<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map<string, FunctionReturn<T>>();
return ((...args: FunctionArgs<T>) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key)!;
const result = fn(...args);
cache.set(key, result);
return result;
}) as T;
}
function expensiveCalc(x: number, y: number): number {
return x * y * Math.PI;
}
const memoCalc = memoize(expensiveCalc);
memoCalc(2, 3); // 类型:number,与原函数完全一致
Level 3 · 规范怎么定义的(资深)
条件类型的核心机制是分布式求值(distributive evaluation):当 T 是裸类型参数且被传入联合类型时,条件类型会对联合的每个成员单独求值。这就是 Exclude<T, U> 能工作的原因——never 在联合中是零元素,会被自动移除。infer 关键字在条件类型的 extends 右侧引入模式匹配变量,编译器会尝试从实际类型中提取匹配该模式的子类型。递归条件类型(TS 4.1)和尾递归优化(TS 4.5)使得 Awaited<T> 等递归解包类型成为可能,但递归深度仍有约 1000 层的限制。
Level 4 · 边界与陷阱(所有人)
反模式:过深的嵌套条件类型
// 反模式:条件类型嵌套超过 3 层,难以阅读和调试
type ComplexType<T> =
T extends string
? T extends `${string}Id`
? T extends `user${string}`
? "userId"
: "otherId"
: "string"
: T extends number
? T extends 0
? "zero"
: "number"
: "other";
// 更好的做法:分拆成独立的命名类型
type ExtractIdType<T extends string> =
T extends `user${string}` ? "userId" : "otherId";
type ExtractStringType<T extends string> =
T extends `${string}Id` ? ExtractIdType<T> : "string";
type ClassifyType<T> =
T extends string ? ExtractStringType<T> :
T extends number ? (T extends 0 ? "zero" : "number") :
"other";
条件类型的最佳实践:
| 规则 | 说明 |
|---|---|
| 拆分命名 | 超过 2 层嵌套就提取为独立类型别名 |
| 注释意图 | 写清楚每个分支的语义 |
| 测试类型 | 用 type Test = Assert<...> 验证边界情况 |
| 知道何时停止 | 如果用普通泛型能解决,不要用条件类型 |
// 辅助工具:类型层面的断言,用于测试
type Assert<T extends true> = T;
type Equals<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
// 验证自定义 Awaited 的行为
type Tests = [
Assert<Equals<Awaited2<Promise<string>>, string>>,
Assert<Equals<Awaited2<Promise<Promise<number>>>, number>>,
Assert<Equals<Awaited2<string>, string>>,
];
// 如果任何一个 Equals 返回 false,这行代码就会报错
总结
| 特性 | 语法 | 用途 |
|---|---|---|
| 基础条件类型 | T extends U ? X : Y |
类型层面的条件判断 |
| 分布式条件类型 | 裸 T 遇到联合类型时自动展开 |
过滤联合类型成员 |
| 阻止分布 | [T] extends [U] |
整体判断联合类型 |
| infer 提取返回值 | T extends (...) => infer R |
推断函数返回类型 |
| infer 提取 Promise 值 | T extends Promise<infer V> |
解包 Promise |
| infer 提取数组元素 | T extends (infer E)[] |
获取元素类型 |
| infer 提取参数 | T extends (...args: infer P) => |
推断参数元组 |
| 递归条件类型 | 类型别名递归引用自身 | 处理任意深度嵌套 |
| never 过滤 | 条件返回 never 以移除联合成员 | 精确过滤联合类型 |
下一章:装饰器与元编程——如何在类和方法上附加编译期和运行时的元数据。