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

手写 MyPick 和 MyPartial

Implement Pick and Partial by hand

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

一句话:映射类型(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. For User that is "id" | "name" | "email".
  • [K in keyof T] — the mapping itself: generate one property per member of the union, with K as the loop variable.
  • ?: — add the optional modifier to the generated property. The same slot also takes readonly; and -? goes the other way — it removes optionality, which is exactly how Required is 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.

TypeScriptMyPartial 与 MyPick示意Illustrative
1interface User {
2 id: number;
3 name: string;
4 email: string;
5}
6
7// MyPartial:把每个属性抄一遍,顺手加上 ?
8type MyPartial<T> = {
9 [K in keyof T]?: T[K];
10};
11
12// 逐段读:
13// keyof T 属性名的联合:"id" | "name" | "email"
14// [K in ...] 映射:对联合里的每个成员各生成一个属性,K 是循环变量
15// ?: 给生成的属性加可选修饰符
16// T[K] 索引访问:原属性是什么类型,就抄什么类型
17
18// MyPick:只保留 K 里列出的属性
19type MyPick<T, K extends keyof T> = {
20 [P in K]: T[P];
21};
22
23type Draft = MyPartial<User>;
24// { id?: number; name?: string; email?: string }
25
26type Card = MyPick<User, "id" | "name">;
27// { id: number; name: string }
28
29// MyPick<User, "age"> 在这一行就报错,而不是悄悄得到一个错的类型
1interface User {
2 id: number;
3 name: string;
4 email: string;
5}
6
7// MyPartial: copy every property and add ? along the way
8type MyPartial<T> = {
9 [K in keyof T]?: T[K];
10};
11
12// Read it piece by piece:
13// keyof T the union of property names: "id" | "name" | "email"
14// [K in ...] mapping: one property per member of the union, K is the loop variable
15// ?: adds the optional modifier to each generated property
16// T[K] indexed access: copy whatever type the original property had
17
18// MyPick: keep only the properties listed in K
19type MyPick<T, K extends keyof T> = {
20 [P in K]: T[P];
21};
22
23type Draft = MyPartial<User>;
24// { id?: number; name?: string; email?: string }
25
26type Card = MyPick<User, "id" | "name">;
27// { id: number; name: string }
28
29// MyPick<User, "age"> fails on this line, instead of quietly giving a wrong type
TypeScript追问:Omit 怎么组合出来示意Illustrative
1// 追问的标准答案:Omit = 先算键的补集,再 Pick
2type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
3
4interface User {
5 id: number;
6 name: string;
7 email: string;
8}
9
10type NoEmail = MyOmit<User, "email">;
11// { id: number; name: string }
12
13// 官方 Omit 的约束更宽:K extends keyof any(即 string | number | symbol)。
14// 所以 Omit<User, "notAKey"> 合法,MyOmit<User, "notAKey"> 报错。
1// The expected answer to the follow-up: Omit is Pick over the remaining keys
2type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
3
4interface User {
5 id: number;
6 name: string;
7 email: string;
8}
9
10type NoEmail = MyOmit<User, "email">;
11// { id: number; name: string }
12
13// The built-in Omit has a looser constraint: K extends keyof any (string | number | symbol).
14// So Omit<User, "notAKey"> is allowed, while MyOmit<User, "notAKey"> is an error.