Utility Types:会用,还要会手写Utility types: use them, and write them yourself
Partial / Pick / Omit / Record 怎么选,mapped type 手写 MyPick 与 MyPartial,conditional type 配 infer 手写 MyReturnType。How to choose between Partial / Pick / Omit / Record, how to write MyPick and MyPartial as mapped types, and how to write MyReturnType with a conditional type and infer.
这一页有什么On this page5
- 01 Partial、Required、Pick、Omit、Record 分别解决什么问题What problem does each of Partial, Required, Pick, Omit and Record solve?
- 02 手写 MyPick 和 MyPartialHow do you write MyPick and MyPartial by hand?
- 03 Exclude、Extract、ReturnType 是怎么实现的How are Exclude, Extract and ReturnType implemented?
- 练习 · 动手做Practice
- 迁移模式Transfer
- 在 patch 参数、props 裁剪、字典三个场景里说出该用哪个 utility type,以及为什么不用索引签名Say which utility type fits each of three cases: a patch argument, trimming props, and a dictionary — and why an index signature is not the answer
- 手写 MyPartial 与 MyPick,并逐符号解释 { [K in keyof T]?: T[K] }Write MyPartial and MyPick by hand, and explain every symbol in { [K in keyof T]?: T[K] }
- 用 Pick 加 Exclude 组合出 Omit,并说出官方 Omit 的约束宽在哪里Build Omit out of Pick and Exclude, and say where the built-in Omit has a looser constraint
- 解释条件类型的分配律,并用 infer 手写 MyReturnTypeExplain how a conditional type distributes over a union, and write MyReturnType with infer
senior 面试几乎不问「Partial 是什么」,问的是「Partial 怎么实现」。会不会 mapped type 和 conditional type,是「用过 TS」和「懂 TS」的分界线 —— 这三道题就压在这条线上。Senior interviews rarely ask what Partial is. They ask how Partial is implemented. Whether you can write a mapped type and a conditional type is the line between having used TypeScript and understanding it. These three questions sit on that line.
Partial、Required、Pick、Omit、Record 分别解决什么问题What problem does each of Partial, Required, Pick, Omit and Record solve?
What problems do Partial, Required, Pick, Omit and Record each solve
一句话:五个都是「按规则从已有类型造新类型」的 工具类型(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 thanRecord<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 toUserlater cannot ripple into it. The opposite direction — dropping fields that must not leak — isOmit<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.
手写 MyPick 和 MyPartialHow do you write MyPick and MyPartial by hand?
Implement Pick and Partial by hand
一句话:映射类型(mapped type)是 「对属性名的联合做一次循环」。{ [K in keyof T]?: T[K] } 这一行就是 Partial 的完整实现:逐个取出 T 的属性名,抄下原类型, 加上可选修饰符。
逐段拆这一行:
keyof T—— 属性名的联合。User的就是"id" | "name" | "email"。[K in keyof T]—— 映射本体: 对联合里的每个成员各生成一个属性,K是循环变量。?:—— 给生成的属性加可选修饰符。 同一个位置还能写readonly;写成-?是反向操作 —— 去掉可选,这正是Required的实现。T[K]—— 索引访问类型(indexed access type): 原属性是什么类型,就抄什么类型。
MyPick 多一个泛型约束(generic constraint):K extends keyof T。没有它,调用方可以给 K 传任何东西,T[P] 也就没法算。有了它,MyPick<User, "age"> 在调用处直接报Type '"age"' does not satisfy the constraint, 而不是悄悄得出一个错的类型。
会追问:「Omit 怎么用 Pick 组合出来?」——Pick<T, Exclude<keyof T, K>>: 先用 Exclude 从全部键里去掉 K,剩下的交给 Pick。 能再补一句「官方 Omit 的约束是 keyof any, 比 keyof T 宽,所以官方版允许去掉一个不存在的键」, 说明你真读过 lib.es5.d.ts —— 背 API 的人和读过实现的人,在这一问上分开。
In one line: a mapped type is one loop over a union of property names. The single line { [K in keyof T]?: T[K] } is the whole implementation of Partial: take each property name of T, copy its original type, add the optional modifier.
Reading it piece by piece:
keyof T— the union of property names. ForUserthat is"id" | "name" | "email".[K in keyof T]— the mapping itself: generate one property per member of the union, withKas the loop variable.?:— add the optional modifier to the generated property. The same slot also takesreadonly; and-?goes the other way — it removes optionality, which is exactly howRequiredis implemented.T[K]— an indexed access type: whatever type the original property had, copy it.
MyPick needs one extra generic constraint: K extends keyof T. Without it, callers may pass anything as K, and T[P] cannot be computed. With it, MyPick<User, "age"> fails right at the call site with Type '"age"' does not satisfy the constraint, instead of quietly producing a wrong type.
Follow-up: “How do you build Omit out of Pick?” — Pick<T, Exclude<keyof T, K>>: Exclude removes K from the full key set, Pick keeps the rest. Add that the official Omit constrains K to keyof any — looser than keyof T, so the official one lets you omit a key that does not exist — and it shows you have actually read lib.es5.d.ts. People who memorized the API and people who read the implementation part ways on this one question.
Exclude、Extract、ReturnType 是怎么实现的How are Exclude, Extract and ReturnType implemented?
How are Exclude, Extract and ReturnType implemented
一句话:三个都是条件类型(conditional type)—— 类型层面的三元表达式 T extends U ? X : Y。 Exclude 和 Extract 靠它的分配律(distributive)过滤联合; ReturnType 再加上 infer,从函数类型里拆出返回值。
分配律:当 T 是裸类型参数、 实参又是联合时,条件类型不把联合当整体判断, 而是逐个成员代入、再把结果并起来。Exclude<"a" | "b" | "c", "a"> 因此展开成三次判断: 命中的变成 never,而 never 是空联合, 并进结果就消失了。「过滤」的本质,是「把不要的变成 never」。
infer:在 extends 右边的模式里挖一个洞, 匹配成功时编译器负责把洞填上。T extends (...args: any[]) => infer R ? R : never里的 R 就是那个洞 —— T 匹配到函数类型时, R 被填成它的返回类型。同一招能拆数组元素、Promise 的值、 函数参数(Parameters 就是把洞挖在参数位置)。
会追问:「怎么关掉分配律?」—— 两边包一层元组:[T] extends [U],T 不再是裸的,联合就被当成整体。 「Exclude<T, T> 是什么?」——never,每个成员都被自己命中。 「never 传进去呢?」—— 还是 never:空联合循环零次。 三问全是分配律的推论,吃透一条规则就全能答。
In one line: all three are conditional types — a ternary at the type level, T extends U ? X : Y. Exclude and Extract filter unions through its distributive behavior; ReturnType adds infer to pull the return type out of a function type.
Distributivity: when T is a bare type parameter and the argument is a union, the conditional does not judge the union as a whole — it substitutes each member separately and unions the results. Exclude<"a" | "b" | "c", "a"> therefore expands into three checks: matching members become never, and never is the empty union, so it vanishes when merged back in. Filtering really means turning the unwanted members into never.
infer: it digs a hole in the pattern to the right of extends, and the compiler fills the hole when the match succeeds. In T extends (...args: any[]) => infer R ? R : never, R is that hole — when T matches a function type, R gets its return type. The same trick unpacks array elements, the value inside a Promise, or function parameters (Parameters is the same hole dug at the parameter position).
Follow-up: “How do you switch distributivity off?” — wrap both sides in a tuple: [T] extends [U], T is no longer bare, so the union is judged as one piece. “What is Exclude<T, T>?” — never: every member is matched by itself. “And feeding never in?” — still never: the empty union loops zero times. All three are corollaries of one rule — master distributivity and the whole set falls out.
动手做Get your hands on it
填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.
面试官给出下面这个类型,问它对 T 做了什么。
An interviewer shows you the type below and asks what it does to T.
换一道题也能用Works on other problems too
考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.
- 五个 utility type 对应五种属性集合操作:变可选、变必填、留白名单、去黑名单、按键集合造字典。The five utility types are five operations on a set of properties: make optional, make required, keep an allow list, drop a deny list, and build a dictionary from a key set.
- Partial 是浅的:只动第一层,嵌套对象内部照样必填。Partial is shallow: it only changes the top level, so fields inside a nested object stay required.
- mapped type 一行四件事:keyof T 取键、in 循环、?/readonly/-? 改修饰符、T[K] 抄类型。A mapped type does four things in one line: keyof T lists the keys, in loops over them, ?/readonly/-? change the modifiers, and T[K] copies the type.
- MyPick 的 K extends keyof T 是泛型约束,把传错键的错误挡在调用处。In MyPick, K extends keyof T is a generic constraint; it reports a wrong key at the call site.
- Omit = Pick<T, Exclude<keyof T, K>>;官方 Omit 的 K 约束是 keyof any,比 keyof T 宽。Omit = Pick<T, Exclude<keyof T, K>>; the built-in Omit constrains K to keyof any, which is looser than keyof T.
- 分配律:裸类型参数遇到联合就逐成员求值再并;never 是空联合,并进去就消失。Distribution: when a bare type parameter meets a union, each member is evaluated separately and the results are joined; never is the empty union, so it disappears from the join.