内置工具类型(下)
本教程共 80 篇 · 第 42 篇 · 更新于 2026-08-10 · 约 12 分钟阅读
本节目标:掌握
Record、Exclude、Extract、NonNullable、ReturnType、Parameters和Awaited——它们的实现涉及条件类型和infer,是 TypeScript 类型体操的”基础招式”。
上一章的工具类型全部基于映射类型实现,没有用到条件判断。这一章的七个工具类型要么用了条件类型,要么用到了更复杂的组合——它们能处理的场景也更”元编程”。
Record<Keys, Type>:批量构造对象类型
它能干什么
Record<Keys, Type> 构造一个对象类型,键名来自 Keys(必须是 string | number | symbol 的子类型),值类型固定为 Type:
type CatName = "miffy" | "boris" | "mordred";
interface CatInfo {
age: number;
breed: string;
}
// 构造一个"猫名 → 猫信息"的映射类型
const cats: Record<CatName, CatInfo> = {
miffy: { age: 10, breed: "Persian" },
boris: { age: 5, breed: "Maine Coon" },
mordred: { age: 16, breed: "British Shorthair" },
};
cats.boris.age; // number ✅
使用 Record 的一个前提:你必须覆盖所有键。上面的例子中,如果少填了 "mordred",编译器会立刻报错。
手写实现原理
type MyRecord<K extends string | number | symbol, T> = {
[P in K]: T;
};
就是映射类型的最简形式——遍历 K 的每个成员,值类型统一为 T。跟前面 Partial 的区别在于:映射的键来源不是 keyof 某个类型,而是一个联合类型 K。
Exclude<UnionType, ExcludedMembers>:从联合中排除
它能干什么
Exclude<UnionType, ExcludedMembers> 从第一个联合类型中剔除能赋值给第二个参数的所有成员:
type T0 = Exclude<"a" | "b" | "c", "a">;
// type T0 = "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">;
// type T1 = "c"
type T2 = Exclude<string | number | (() => void), Function>;
// type T2 = string | number
第三个例子值得留意——(() => void) 是 Function 的子类型,所以被排除,剩下 string | number。
手写实现原理
type MyExclude<T, U> = T extends U ? never : T;
这里用到了条件类型的**分布(distributive)**特性。当 T 是联合类型时,T extends U ? never : T 会被分配到联合的每一个成员上:
Exclude<"a" | "b" | "c", "a">
→ ("a" extends "a" ? never : "a") | ("b" extends "a" ? never : "b") | ("c" extends "a" ? never : "c")
→ never | "b" | "c"
→ "b" | "c" (never 在联合中自动消失)
Note
never在联合类型中会被自动吸收——就像数学里的”空集”。任何类型跟never做联合,结果还是那个类型本身。
Extract<Type, Union>:从联合中提取
它能干什么
Extract<Type, Union> 是 Exclude 的反向——只保留能赋值给第二个参数的那些成员:
type T0 = Extract<"a" | "b" | "c", "a" | "f">;
// type T0 = "a" ——"a" 是交集,"b" 和 "c" 都不在右边
type T1 = Extract<string | number | (() => void), Function>;
// type T1 = () => void ——只有函数类型是 Function 的子类型
手写实现原理
type MyExtract<T, U> = T extends U ? T : never;
跟 Exclude 的区别只是把 never : T 换成了 T : never。Exclude 是在”匹配时抛弃”,Extract 是在”匹配时保留”。
NonNullable<Type>:排除 null 和 undefined
它能干什么
NonNullable<Type> 从联合类型中排除 null 和 undefined:
type T0 = NonNullable<string | number | undefined>;
// type T0 = string | number
type T1 = NonNullable<string[] | null | undefined>;
// type T1 = string[]
type T2 = NonNullable<string | null>;
// type T2 = string
非常实用的场景:处理后端返回的”可能为空”的数据时,用 NonNullable 做一层类型收窄。
手写实现原理
type MyNonNullable<T> = T extends null | undefined ? never : T;
就是 Exclude 的具体化——把 U 固定为 null | undefined。
ReturnType<Type>:取函数返回值类型
它能干什么
ReturnType<Type> 从函数类型中提取返回值类型:
function greet(name: string): string {
return `你好,${name}`;
}
type GreetReturn = ReturnType<typeof greet>;
// type GreetReturn = string
type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<<T>() => T>; // unknown
Note对于重载函数,
ReturnType取的是最后一个重载签名的返回值类型——也就是最宽泛的那一个。
手写实现原理
type MyReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : never;
这里出现了新关键词 infer。它的作用是:在条件类型的 extends 分支中声明一个临时的类型变量,让 TypeScript 去推断它。
拆解一下:
T extends (...args: any) => infer R:如果T是一个函数类型,就把它的返回值类型捕获到变量R里。? R : never:如果能匹配,返回R;否则返回never。
infer 是 TypeScript 类型系统中的”反推”能力——你不用手动把函数签名拆开去读返回值,infer 帮你做这件事。
Parameters<Type>:取函数参数元组
它能干什么
Parameters<Type> 从函数类型中提取参数列表,结果是一个元组类型:
function greet(name: string, age: number): string {
return `${name} 今年 ${age} 岁`;
}
type GreetParams = Parameters<typeof greet>;
// type GreetParams = [name: string, age: number]
type T0 = Parameters<() => string>; // []
type T1 = Parameters<(s: string) => void>; // [s: string]
type T2 = Parameters<<T>(arg: T) => T>; // [arg: unknown]
手写实现原理
type MyParameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
跟 ReturnType 思路一致,只是 infer 的位置不同:
ReturnType在返回值位置infer R。Parameters在参数位置infer P。
Awaited<Type>:递归解包 Promise
它能干什么
Awaited<Type> 模拟 await 的行为——递归解开 Promise 包裹,直到拿到最内层的值类型:
type A = Awaited<Promise<string>>;
// type A = string
type B = Awaited<Promise<Promise<number>>>;
// type B = number ——两层 Promise,全部解包
type C = Awaited<boolean | Promise<number>>;
// type C = boolean | number
手写实现原理
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;
关键点:
T extends Promise<infer U>:如果T是Promise<U>,提取内层类型U。? MyAwaited<U>:递归调用自己——如果U又是一个Promise,那就继续解包。: T:如果不是Promise,直接返回T。
注意:TypeScript 的类型递归有限制,但 Awaited 这种”一层层剥开 Promise”的递归是官方支持的。
小结
这七个工具类型的实现本质是一句话:条件类型 T extends U ? X : Y 配合 infer 做模式匹配。
| 工具类型 | 核心机制 | 关键词 |
|---|---|---|
Record<K, T> | 映射类型 | 无 |
Exclude<T, U> | 分布式条件 | extends + never |
Extract<T, U> | 分布式条件 | extends + T |
NonNullable<T> | 分布式条件 | extends null|undefined |
ReturnType<T> | 条件类型 + infer | infer R |
Parameters<T> | 条件类型 + infer | infer P |
Awaited<T> | 递归 + infer | infer U + 递归 |
infer 是理解后三个工具类型的关键——它让你在”模式匹配”的过程中从类型里抓出想要的片段。下一章我们正式深入条件类型,看看分布特性和 never 过滤到底是怎么工作的。