DrillLab
八股题库Question bank

105 道问答题,一道一卡105 questions, one card each

默认只显示问题 —— 先自己在心里答一遍,再展开对答案。答不上来就标「不会」,下次抽认卡会先抽它。Only the question shows by default. Answer it in your head first, then open the answer. Mark the ones you miss; the flashcard round puts those first.

0 / 105道自评过self-assessed
0Got it0模糊Shaky0不会No idea105还没做Not seen
正在读这台浏览器里的标记…Reading your marks from this browser…
标记不是打分,是给下一轮排队Marks are a queue, not a score

标「不会」的题会排到抽认卡最前面,标「会」的进低频池排最后。所以别客气 —— 觉得答得磕磕巴巴就标「模糊」。准备好了就去抽认卡“No idea” jumps to the front of the next round; “got it” drops to the back. So be honest — if it came out shaky, mark it shaky. Then go run a flashcard round.

找一道题Find one
按方向、掌握状态筛Filter by topic and markTypeScript 深度TypeScript deep dive

题目Questions

筛出 3 道。3 of 3 questions.
TypeScript 深度TypeScript deep diveDrillLab 自出By DrillLab

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

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

看答案Show answer

一句话:五个都是「按规则从已有类型造新类型」的 工具类型(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
TypeScript 深度TypeScript deep diveDrillLab 自出By DrillLab

手写 MyPick 和 MyPartial

Implement Pick and Partial by hand

看答案Show answer

一句话:映射类型(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.
TypeScript 深度TypeScript deep diveDrillLab 自出By DrillLab

Exclude、Extract、ReturnType 是怎么实现的

How are Exclude, Extract and ReturnType implemented

看答案Show answer

一句话:三个都是条件类型(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.

TypeScript分配律:过滤联合示意Illustrative
1// 条件类型:类型层面的三元表达式
2type IsString<T> = T extends string ? true : false;
3type A = IsString<"hi">; // true
4type B = IsString<42>; // false
5
6// 分配律:裸类型参数遇到联合,逐个成员代入,再把结果并起来
7type NoA = Exclude<"a" | "b" | "c", "a">;
8// = ("a" extends "a" ? never : "a")
9// | ("b" extends "a" ? never : "b")
10// | ("c" extends "a" ? never : "c")
11// = never | "b" | "c"
12// = "b" | "c" ← never 是空联合,并进去就消失
13
14// 手写版各一行
15type MyExclude<T, U> = T extends U ? never : T;
16type MyExtract<T, U> = T extends U ? T : never;
1// A conditional type: the ternary expression, at the type level
2type IsString<T> = T extends string ? true : false;
3type A = IsString<"hi">; // true
4type B = IsString<42>; // false
5
6// Distribution: a bare type parameter over a union is applied member by member, then joined
7type NoA = Exclude<"a" | "b" | "c", "a">;
8// = ("a" extends "a" ? never : "a")
9// | ("b" extends "a" ? never : "b")
10// | ("c" extends "a" ? never : "c")
11// = never | "b" | "c"
12// = "b" | "c" ← never is the empty union, so it disappears in a join
13
14// One line each, written by hand
15type MyExclude<T, U> = T extends U ? never : T;
16type MyExtract<T, U> = T extends U ? T : never;
TypeScriptinfer:把返回类型拆出来示意Illustrative
1// infer R:在匹配的模式里挖一个洞,编译器匹配成功时负责填上
2// (lib.es5.d.ts 里的 ReturnType 就是这么写的)
3type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
4
5function getUser() {
6 return { id: 1, name: "Ada" };
7}
8
9type U = MyReturnType<typeof getUser>;
10// { id: number; name: string }
11
12// 同一招能拆任何复合类型
13type ElementOf<T> = T extends (infer E)[] ? E : never;
14type Unwrap<T> = T extends Promise<infer V> ? V : T;
15
16type N = ElementOf<number[]>; // number
17type S = Unwrap<Promise<string>>; // string
1// infer R: leave a hole in the pattern, and the compiler fills it in when the match succeeds
2// (this is exactly how ReturnType is written in lib.es5.d.ts)
3type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
4
5function getUser() {
6 return { id: 1, name: "Ada" };
7}
8
9type U = MyReturnType<typeof getUser>;
10// { id: number; name: string }
11
12// The same move takes apart any composite type
13type ElementOf<T> = T extends (infer E)[] ? E : never;
14type Unwrap<T> = T extends Promise<infer V> ? V : T;
15
16type N = ElementOf<number[]>; // number
17type S = Unwrap<Promise<string>>; // string

这些题从哪来Where these come from

99 道来自面试题库 #269–#387;TypeScript 深度那 6 道是 DrillLab 自出的(senior 补强,卡片上有标注)。答案都是 DrillLab 写的,所以讲解里的代码块一律标「示意」。每道题都能点回它出处的那一节课。99 questions come from the interview bank (#269–#387); the 6 TypeScript deep-dive ones are DrillLab-made (marked on the card). All answers are written by DrillLab, so every code block here is labelled “demo”. Each card links back to the lesson it came from.