DrillLab
第 100 / 105 道100 / 105 · DrillLab 自出By DrillLab

Partial、Required、Pick、Omit、Record 分别解决什么问题

What problems do Partial, Required, Pick, Omit and Record each solve

先自己答,再往下看Answer it yourself first

一句话:五个都是「按规则从已有类型造新类型」的 工具类型(utility types):Partial 全变可选、Required 全变必填、Pick 只留白名单、Omit 去掉黑名单、Record 按键集合造字典。 考点不是背 API,是看到场景知道伸手拿哪个。

三个高频场景:

  • 更新函数的 patch 参数 → Partial<User>调用方只传要改的字段,但传了的字段类型必须对, 拼错字段名照样编译报错 (Object literal may only specify known properties)。 这是它比 Record<string, unknown> 强的地方: 可选,但没有放弃字段级检查。
  • 从大类型裁 props → Pick<User, "name" | "role">组件只声明真正用到的字段,User 以后加字段不会波及它。 反方向的「去掉不该外泄的字段」用Omit<User, "email">
  • 键集合已知的字典 → Record<Theme, string>, 别用索引签名。键是字面量联合(literal union)时,Record 的键集合是封闭的: 少一个键、多一个键都是编译错误。 索引签名对任何 string 键都放行,还谎称读出来的值一定存在。

Pick 和 Omit 怎么选:白名单还是黑名单。 类型以后会加字段时,Omit 会把新字段自动带进来 —— 透传配置时这是优点,做脱敏时这是漏洞。 所以对外暴露的公开类型建议用 Pick: 新加的敏感字段默认不外泄。

会追问:「Partial 是深的还是浅的?」—— 浅的, 只动第一层,嵌套对象内部照样必填;要深的得自己写递归的 mapped type。 「Record<string, T> 和索引签名什么区别?」—— 几乎等价,但 Record 的键能用字面量联合,索引签名不行。 答完这两问,多半会接一句「那 Partial 自己怎么实现?」—— 那就是下一题。

In one line: all five are utility types — they build a new type from an existing one by a rule: Partial makes everything optional, Required makes everything required, Pick keeps a whitelist, Omit drops a blacklist, Record builds a dictionary from a key set. The real question is not the API — it is knowing which one a scenario calls for.

Three scenarios that keep coming up:

  • The patch parameter of an update function → Partial<User>. Callers pass only the fields they change, but every field they do pass is still checked — a typo still fails to compile (Object literal may only specify known properties). That is what makes it stronger than Record<string, unknown>: optional, without giving up per-field checking.
  • Trimming props from a big type → Pick<User, "name" | "role">. The component declares only the fields it actually uses, so adding fields to User later cannot ripple into it. The opposite direction — dropping fields that must not leak — is Omit<User, "email">.
  • A dictionary with a known key set → Record<Theme, string>, not an index signature. With a literal union as the key, Record is a closed key set: one key missing or one key extra is a compile error. An index signature waves any string through, then claims the value definitely exists.

Choosing between Pick and Omit: whitelist or blacklist. When the type will grow, Omit silently carries every new field along — a feature when forwarding config, a hole when redacting. So for public-facing types, prefer Pick: a newly added sensitive field stays private by default.

Follow-up: “Is Partial deep or shallow?” — shallow. It only touches the first level; properties inside nested objects stay required. A deep version means writing a recursive mapped type yourself. “What is the difference between Record<string, T> and an index signature?” — almost none, except Record keys can be literal unions and index signatures cannot. Answer both, and the next question is usually “so how is Partial itself implemented?” — which is exactly the next card.

TypeScript三个场景各拿哪个示意Illustrative
1interface User {
2 id: number;
3 name: string;
4 email: string;
5 role: "admin" | "member";
6}
7
8// Partial<T>:patch 的每个字段都可以不传,但传了就必须对
9function updateUser(user: User, patch: Partial<User>): User {
10 return { ...user, ...patch };
11}
12
13declare const user: User;
14updateUser(user, { name: "Ada" }); // ✓ 只传要改的字段
15// updateUser(user, { nmae: "Ada" }); // ✗ 拼错字段名,编译报错
16
17// Pick<T, K>:从大类型裁出组件真正用到的字段
18type UserCardProps = Pick<User, "name" | "role">;
19
20// Omit<T, K>:去掉不该外泄的字段
21type PublicUser = Omit<User, "email">;
1interface User {
2 id: number;
3 name: string;
4 email: string;
5 role: "admin" | "member";
6}
7
8// Partial<T>: every field in patch may be left out, but if given it must be right
9function updateUser(user: User, patch: Partial<User>): User {
10 return { ...user, ...patch };
11}
12
13declare const user: User;
14updateUser(user, { name: "Ada" }); // ✓ pass only the fields you change
15// updateUser(user, { nmae: "Ada" }); // ✗ field name misspelt, compile error
16
17// Pick<T, K>: cut the fields a component really uses out of a bigger type
18type UserCardProps = Pick<User, "name" | "role">;
19
20// Omit<T, K>: drop the fields that must not leak out
21type PublicUser = Omit<User, "email">;
TypeScriptRecord vs 索引签名示意Illustrative
1type Theme = "light" | "dark";
2
3// Record + 字面量联合:键集合是封闭的
4const themeColor: Record<Theme, string> = {
5 light: "#ffffff",
6 dark: "#1a1a1a",
7};
8// 少写 dark、多写 blue,都是编译错误
9
10// 索引签名:任何 string 键都「合法」
11const loose: { [key: string]: string } = { light: "#ffffff" };
12loose["drak"]; // 编译器放行,类型还谎称是 string —— 运行时是 undefined
1type Theme = "light" | "dark";
2
3// Record plus a literal union: the set of keys is closed
4const themeColor: Record<Theme, string> = {
5 light: "#ffffff",
6 dark: "#1a1a1a",
7};
8// Leaving out dark, or adding blue, is a compile error either way
9
10// An index signature: any string key is "valid"
11const loose: { [key: string]: string } = { light: "#ffffff" };
12loose["drak"]; // The compiler allows it and claims string — at runtime it is undefined