Partial<T> 是一个内置工具类型,它把 T 的每个属性都变成可选。它天然就是更新或补丁操作的类型:调用者只传入发生变化的字段。
Partial<User> 就是 { id?: number; name?: string; email?: string }。编译器仍然会检查你传入的字段:{ nmae: "x" } 或 { name: 42 } 都会报错,这正是 Partial 比宽松的 object 或 any 参数更好的地方。
Partial 是如何定义的
Partial 是 TypeScript 标准库里一个只有一行的映射类型:
type Partial<T> = {
[P in keyof T]?: T[P];
};
对于 T 的每个键 P,它声明一个类型相同的可选属性。由于它是对 keyof T 做映射,原本带 readonly 的属性会保留 readonly。它只是一个普通类型,运行时没有任何作用:作为 changes 传入的对象无论如何都是同一个对象。
读取 Partial 的属性得到 T | undefined
Partial<T> 的每个属性都可能不存在,所以读取时得到的是属性类型加上 undefined。编译器会要求你处理缺失的情况:
{ ...defaults, ...opts } 是把 Partial<Options> 还原成完整 Options 的常用写法:先展开默认值,再让传入的值覆盖它们。
显式 undefined 的陷阱
可选属性可以不存在,但也可以存在并且值为 undefined。对象展开会用这个 undefined 覆盖真实的值,而结果的类型看不出来:
当补丁来自表单或查询字符串,空字段变成 undefined 时,就会踩到这个坑。编译选项 exactOptionalPropertyTypes(不包含在 strict 中)会让 { name: undefined } 对于 name?: string 成为编译错误,除非你写成 name?: string | undefined,从源头上避免这个问题。
Partial 是浅层的
Partial 只让顶层属性变成可选。嵌套对象如果传入,就必须是完整的:
index.ts(8,3): error TS2741: Property 'tabSize' is missing in type '{ fontSize: number; }' but required in type '{ fontSize: number; tabSize: number; }'.
editor 属性是可选的,但一旦出现,它的类型仍然是 { fontSize: number; tabSize: number },没有任何变化。对于设置对象、API 补丁和测试数据,你常常希望每一层的属性都是可选的。这就需要递归类型。
DeepPartial:递归的 Partial
TypeScript 没有内置的深层版本,但写起来只需几行。函数和数组保持原样,因为把数组元素变成可选会允许 [undefined]:
类型是递归的,合并却不是。applySettings 手动合并 editor,因为对象展开同样是浅层的。lodash 等库提供了通用的深层合并函数(merge),而它的类型定义比上面的类型难写得多。
Required:Partial 的反面
Required<T> 去掉每个属性的 ?。它是用 -? 修饰符定义的,这个修饰符同时会从每个属性的类型中去掉 undefined:
这个模式和 Partial 一样,只是方向相反:API 的使用者传入宽松的配置,内部代码则使用 Required 版本,每个值都确定存在。这里的展开和上面的更新函数有同样的漏洞:调用者如果显式传入 port: undefined,就会用 undefined 覆盖默认值,而只要没开启 exactOptionalPropertyTypes,编译器就会接受。Required 和 Partial 一样是浅层的。
只让部分属性变成可选或必选
Partial 和 Required 作用于每个属性。如果只想改变其中几个,就用 Pick 和 Omit 把类型拆开再拼回去:
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
interface Post {
id: number;
title: string;
body?: string;
}
type NewPost = PartialBy<Post, "id">; // id optional, title required, body optional
type Published = RequiredBy<Post, "body">; // body now required
| 类型 | 作用 | 是否深层 |
|---|---|---|
Partial<T> | 每个属性都变为可选 | 否 |
Required<T> | 每个属性都变为必选,并去掉 undefined | 否 |
DeepPartial<T>(自己定义) | 每一层都可选 | 是 |
PartialBy<T, K>(自己定义) | 只有键 K 变为可选 | 否 |
Readonly<T> | 每个属性都变为 readonly | 否 |
常见问题
TypeScript 中的 Partial 有什么作用?
Partial<T> 会创建一个类型,把 T 的所有属性都标记为可选。对于 interface User { name: string; email: string },Partial<User> 就是 { name?: string; email?: string },所以 {}、{ name: "Ada" } 和完整的用户对象都是合法的值。
TypeScript 的 Partial 是深层的吗?
不是,Partial 只影响顶层属性。Partial<T> 里的嵌套对象仍然必须是完整的。如果需要递归版本,就写一个 DeepPartial<T> 类型,让它对对象类型的属性递归应用自身。
TypeScript 中 Partial 的反面是什么?
Required<T>。它去掉每个属性的 ?,同时从属性类型中去掉 undefined,所以 Required<{ port?: number }> 就是 { port: number }。它是用带 -? 修饰符的映射类型定义的。
如何只让部分属性变成可选?
组合使用 Omit、Pick 和 Partial:type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>。PartialBy<User, "email"> 保持其他属性不变,只有 email 变成可选。
为什么合并 Partial 更新之后,某个属性变成了 undefined?
Partial<T> 允许属性存在但值为 undefined,而对象展开会把它复制过去:{ ...user, ...{ name: undefined } } 的结果里是 name: undefined,尽管 TypeScript 把结果的类型标为 User。合并之前先过滤掉 undefined 值,或者开启 exactOptionalPropertyTypes,让显式的 undefined 被拒绝。