常用 Utility Types(工具类型)
下文默认基于 TypeScript 5.x,并假设开启
strict: true。
面试速答(30 秒版 TL;DR)
- Utility Types 是 TypeScript 内置的一批“类型加工工具”,本质上主要由映射类型、条件类型和
infer组成。 - 最常用的几组:
- 对象形状变换:
Partial、Required、Readonly、Pick、Omit、Record - 联合过滤:
Exclude、Extract、NonNullable - 函数 / 类推导:
Parameters、ReturnType、ConstructorParameters、InstanceType - Promise 展开:
Awaited
- 对象形状变换:
- 面试别只背名字,最好能说出:
- 解决什么问题;
- 底层大概怎么实现;
- 哪些是浅层处理,哪些容易误用。
心智模型:工具类型不是新语法,而是“类型积木”
所以标准答法通常不是“它们是编译器黑魔法”,而是:
- 编译器内置了几种很常用的类型模板;
- 你也可以用同样思路自己写一套业务工具类型。
1. 对象形状变换类
1.1 Partial<T>
把所有属性变成可选:
interface User {
id: string;
name: string;
age: number;
}
type UserPatch = Partial<User>;
适合:
- 表单草稿;
- 更新接口的 patch 参数;
- 局部覆写配置。
底层直觉:
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
注意:它是浅层的,只改第一层属性是否可选,不会递归深入。
1.2 Required<T>
把所有可选属性改为必填:
type FullUser = Required<Partial<User>>;
适合把“输入阶段可缺省”的类型,恢复成“处理阶段必须完整”的类型。
1.3 Readonly<T>
把所有属性变成只读:
type ReadonlyUser = Readonly<User>;
它约束的是类型层面的写操作,不是运行时冻结对象。
1.4 Pick<T, K> 与 Omit<T, K>
type UserPreview = Pick<User, "id" | "name">;
type UserWithoutAge = Omit<User, "age">;
它们适合从大对象里切出视图模型。
面试里可以补一句:
Omit不是编译器底层原语,本质也是在Pick的基础上组合出来的。
1.5 Record<K, V>
type UserMap = Record<string, User>;
type StatusLabel = Record<"idle" | "loading" | "done", string>;
适合表达“键到值的映射”。
如果 K 是字面量联合,它还能帮你检查键是否写全。
2. 联合过滤类
2.1 Exclude<T, U>
从联合里剔除满足 U 的成员:
type T = Exclude<"a" | "b" | "c", "a" | "c">; // "b"
底层思路:
type MyExclude<T, U> = T extends U ? never : T;
2.2 Extract<T, U>
保留满足 U 的成员:
type T = Extract<"a" | "b" | "c", "a" | "c">; // "a" | "c"
2.3 NonNullable<T>
去掉 null | undefined:
type Name = NonNullable<string | null | undefined>; // string
这是业务里非常高频的一个工具类型,常配合接口返回值做兜底收窄。
3. 函数与类推导类
3.1 Parameters<T>
提取函数参数元组:
function fetchUser(id: string, retry?: number) {
return { id, retry };
}
type FetchUserArgs = Parameters<typeof fetchUser>;
// [id: string, retry?: number | undefined]
3.2 ReturnType<T>
提取函数返回值:
type FetchUserResult = ReturnType<typeof fetchUser>;
// { id: string; retry: number | undefined }
3.3 ConstructorParameters<T> 与 InstanceType<T>
class Person {
constructor(public name: string, public age: number) {}
}
type PersonCtorArgs = ConstructorParameters<typeof Person>;
type PersonInstance = InstanceType<typeof Person>;
这组工具特别适合做工厂函数、依赖注入、类包装器。
底层思路通常依赖 infer:
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
4. Promise 相关:Awaited<T>
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number
它的价值在于:
- 不只是拆一层
Promise; - 对嵌套 Promise 或 Promise-like 也能持续展开。
这在你写异步工具函数、封装请求层时非常常见。
5. 一张表快速归类
| 工具类型 | 作用 | 高频场景 |
|---|---|---|
Partial<T> | 全部属性可选 | patch 更新、表单草稿 |
Required<T> | 全部属性必填 | 进入核心处理流程前收紧类型 |
Readonly<T> | 全部属性只读 | 配置对象、只读输入 |
Pick<T, K> | 选择部分字段 | 视图模型、DTO 裁剪 |
Omit<T, K> | 排除部分字段 | 隐藏敏感字段、派生展示类型 |
Record<K, V> | 键值映射 | 状态字典、配置表 |
Exclude<T, U> | 从联合中排除 | 过滤非法状态 |
Extract<T, U> | 从联合中提取 | 提取某类事件 / 某类分支 |
NonNullable<T> | 去掉空值 | 接口数据收窄 |
Parameters<T> | 提取参数元组 | 包装函数、透传参数 |
ReturnType<T> | 提取返回值 | 复用函数结果类型 |
InstanceType<T> | 提取实例类型 | 类工厂、IOC |
Awaited<T> | 展开 Promise | 异步返回值推导 |
6. 高频题标准答法
6.1 Utility Types 的本质是什么
标准口径:
- 它们是 TypeScript 内置的通用类型模板;
- 大多数基于映射类型、条件类型和
infer实现; - 目的是减少重复声明,提高类型复用性。
6.2 Partial 和 Readonly 是深层的吗
默认都不是。
它们只处理第一层属性。嵌套对象内部字段是否可选、是否只读,不会自动递归。
6.3 Pick 和 Omit 怎么选
- 明确要哪些字段时,用
Pick; - 明确不要哪些字段时,用
Omit。
如果原始类型会持续演进,很多团队更偏向 Pick,因为它更显式,也更不容易因为新增字段而误带出去。
7. 常见追问
7.1 Record<string, T> 能不能保证对象键只有这些
不能。
string 太宽了,它表示任意字符串键,不是有限集合。
如果你想让键集合受控,应该把 K 写成字面量联合。
7.2 ReturnType 遇到重载怎么办
通常会以最后一个重载签名作为推导基础,所以复杂重载场景下要小心结果是否符合预期。
7.3 为什么很多团队会自己写 DeepPartial
因为内置 Partial 只处理浅层,而真实业务里经常有深层嵌套配置对象,需要递归可选化。
易错点 / 坑
- 以为
Partial、Readonly是深层处理,结果嵌套对象还是原样。 - 滥用
Omit,把原始模型和派生模型关系搞得越来越隐晦。 - 把
Record<string, T>当成“有限 key 枚举”,实际上它只是在说“任意字符串都能当键”。 - 遇到
ReturnType、Parameters推导不对时,不知道看函数是否有重载或泛型约束。
速记要点(可背诵)
- 工具类型本质是映射类型、条件类型、
infer的现成封装。 - 对象改形状:
Partial、Required、Readonly、Pick、Omit、Record。 - 联合做过滤:
Exclude、Extract、NonNullable。 - 函数 / 类 / Promise 做提取:
Parameters、ReturnType、InstanceType、Awaited。 - 内置工具类型大多是浅层处理,复杂业务经常需要自定义扩展版。