TypeScript Utility Types
把 Partial、Required、Pick、Omit、Record、Readonly、ReturnType、Parameters、Awaited 等内置 Utility Types 按裁剪、映射与推导三类理解,并说明它们与 keyof、映射类型和条件类型的关系。
#synthesis
#evergreen
#dev
#typescript
[!info] related notes
- 所属 MOC: typescript-moc、typescript-learning-roadmap
- 相关概念:
- 易混淆概念:
- 相关资源:
TypeScript Utility Types
核心定位
Utility Types 不是一组需要逐个死背的魔法类型。
更稳定的理解是:
它们是 TypeScript 已经替你写好的常见类型变换。
底层主要建立在:
- 泛型;
keyof;- 映射类型;
- 条件类型;
infer;
之上。
三类最常用能力
裁剪 / 修改对象属性
Partial<T>:全部属性可选;Required<T>:全部属性必选;Pick<T, K>:只保留指定键;Omit<T, K>:排除指定键;Readonly<T>:全部属性只读。
interface User {
id: number
name: string
email: string
}
type UpdateUser = Partial<Pick<User, "name" | "email">>
建立键到值的映射
Record<K, V> 用一组 key 构造对象映射:
type Role = "admin" | "editor" | "viewer"
type RoleLabels = Record<Role, string>
从既有类型推导
ReturnType<T>:函数返回值;Parameters<T>:函数参数元组;Awaited<T>:展开 Promise / thenable 的结果类型。
type ApiResult = ReturnType<typeof fetchUser>
type Args = Parameters<typeof fetchUser>
type Value = Awaited<Promise<string>>
为什么它们比重复写 DTO 更稳
当新类型本质上是已有契约的变换时:
重新手写第二份结构
→ 两份定义可能漂移
从已有类型推导
→ 变化沿类型关系传播
因此 Utility Types 的真正价值是表达类型之间的关系,而不只是减少几行代码。
不要过度组合
如果一个类型需要套很多层:
Partial<Omit<Pick<...>>>
并且业务语义已经看不清,应考虑:
- 给中间类型命名;
- 直接声明领域契约;
- 检查是否把内部实体模型泄漏到 API/UI 边界。
“能推导”不等于“应该推导”。
手写理解
type MyPartial<T> = {
[K in keyof T]?: T[K]
}
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
这里真正要理解的是:
keyof T得到 key 集合;in遍历 key;- 映射类型重新构造对象属性。
学习检查
不看正文回答:
- Utility Types 的本质为什么是“类型变换”而不是一组独立语法?
Pick与Omit分别从什么方向裁剪对象类型?Record<K, V>与普通随手写对象类型相比,什么时候更能表达约束?ReturnType/Parameters为什么能减少重复定义?Partial<Pick<T, K>>表达了怎样的类型关系?- 为什么过度嵌套 Utility Types 反而会降低领域可读性?
- 手写一个最小
Readonly<T>或Pick<T, K>,解释每一段语法。