常见 TypeScript 工具类型有哪些?
简化版
常见工具类型有 Partial、Required、Readonly、Pick、Omit、Record、Exclude、Extract、ReturnType、Parameters。它们本质上是基于泛型、keyof、映射类型和条件类型做类型变换。
详细版
示例:
interface User {
id: number;
name: string;
age?: number;
}
type UserPatch = Partial<User>;
type UserPreview = Pick<User, 'id' | 'name'>;
type UserWithoutAge = Omit<User, 'age'>;
type UserMap = Record<string, User>;
函数相关:
type Fn = (id: number) => Promise<User>;
type Args = Parameters<Fn>;
type Ret = ReturnType<Fn>;
面试要能说出用途,而不只是背名字。
完整版教学
一、工具类型是类型层面的复用函数
业务中经常需要从已有类型派生新类型。比如创建用户需要一套字段,更新用户允许部分字段,列表预览只需要 id 和 name。
如果每次手写,很容易和源类型不一致。工具类型能保持关联。
二、对象字段变换
Partial<T> 把所有属性变可选,适合更新参数:
function updateUser(id: number, patch: Partial<User>) {}
Readonly<T> 把属性变只读,适合表达不可直接修改。
三、字段挑选和排除
Pick<T, K> 从类型中挑字段;Omit<T, K> 排除字段。
type PublicUser = Omit<User, 'password'>;
这比重新定义 PublicUser 更安全,因为 User 字段变化时能同步影响派生类型。
四、联合类型变换
Exclude<A, B> 从联合类型 A 中排除能赋给 B 的类型;Extract<A, B> 保留能赋给 B 的类型。
type A = Exclude<'a' | 'b', 'a'>; // 'b'
五、面试追问与工程落地
工具类型常见追问是“Partial 是深层可选吗”。内置 Partial<T> 只处理第一层属性,不会递归深入嵌套对象。如果需要深层可选,要自己写 DeepPartial 或使用工具库类型,但深层工具类型会增加类型复杂度。
还会问 Omit 怎么实现。常见实现是 Pick<T, Exclude<keyof T, K>>,这能体现工具类型之间是可组合的,而不是一堆魔法关键字。
工程里工具类型适合从源类型派生变体,但不要把接口请求、表单、数据库实体全部强行共用一个类型再疯狂 Omit/Pick。不同层模型语义不同,过度复用会让类型越来越难读。
六、拆开映射、选键和分发三个底层动作
多数内置工具类型不是编译器关键字,而是 lib.d.ts 中的泛型类型别名。对象字段变换依赖映射类型,联合筛选依赖分发条件类型,函数信息提取依赖条件类型与 infer。理解这些积木后,遇到陌生工具类型也能从定义推导结果。
type MyPartial<T> = { [K in keyof T]?: T[K] }
type MyPick<T, K extends keyof T> = { [P in K]: T[P] }
type MyExclude<T, U> = T extends U ? never : T
type MyOmit<T, K extends PropertyKey> = MyPick<T, Exclude<keyof T, K>>
| 工具类别 | 代表类型 | 核心机制 |
|---|---|---|
| 属性修饰 | Partial、Required、Readonly | 映射类型修改第一层属性标记 |
| 键集合 | Pick、Omit、Record | keyof 与键映射 |
| 联合筛选 | Exclude、Extract、NonNullable | 分发条件类型 |
| 函数提取 | Parameters、ReturnType | infer 匹配签名 |
| 异步解包 | Awaited | 递归模拟 await 解包语义 |
假设 User 有 5 个字段,Pick<User, 'id' | 'name'> 只映射其中 2 个键;源字段类型变化后,这 2 个派生字段会同步变化。Partial<User> 仍只修改最外层 5 个属性,嵌套对象内部不会自动变成可选或只读。
ReturnType、Parameters 面对重载函数时通常依据最后一个签名推断,结果可能是宽泛联合。Awaited 比简单的 T extends Promise<infer U> 更完整,会递归解包 thenable,并正确处理联合与空值语义。
记忆路径:先问是在改“对象的键”、筛“联合的成员”,还是拆“函数/Promise 的结构”,再选择对应工具,而不是从名字列表里碰运气。
七、常见误区与追问
- 误区:Partial 和 Readonly 会递归处理所有嵌套属性。 内置版本只处理第一层,深层变换需要另写递归类型并评估复杂度。
- 误区:Omit 会在运行时删除对象字段。 工具类型只改变静态视图,不会生成删除属性的 JavaScript。
- 误区:Record<string, T> 能保证运行时任意字符串键都有值。 它描述索引形状;动态读取的空缺风险还取决于数据和
noUncheckedIndexedAccess配置。 - 追问:Omit 可以怎样实现? 先用
Exclude<keyof T, K>排除键,再用Pick映射保留的键。 - 追问:Partial 为什么是浅层的? 映射只遍历
keyof T并给当前属性加可选标记,没有递归进入T[K]。 - 追问:Awaited 与手写 Promise 解包有什么差异? 内置类型递归模拟 await 对 PromiseLike 的处理,并覆盖联合、null、undefined 等边界。
- 追问:什么时候不应继续叠加工具类型? 当不同层模型语义已经分离,或组合让报错和字段来源难以理解时,应定义清晰的独立类型。
八、加强记忆
工具类型的核心是“别复制类型,派生类型”。对象字段用 Partial/Pick/Omit/Record,联合筛选用 Exclude/Extract,函数信息用 Parameters/ReturnType。
能够从映射类型、keyof、条件分发和 infer 还原定义,比记住十几个名称更能应对变体追问。