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 mark

题目Questions

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

为什么 getProp 必须写 K extends keyof T

Why does getProp need the constraint K extends keyof T

看答案Show answer

一句话:这个约束向编译器证明 「K 一定是 T 的属性名之一」。有了它,obj[key]才编译得过,返回类型才能精确写成 T[K] —— 一个约束同时解决合法性和精确性两件事。

不写约束报什么错:Type 'K' cannot be used to index type 'T'.不加约束时 K 和 T 之间没有任何关系, 编译器无法证明 obj 上存在这个键,索引表达式直接被拒。 这是泛型题里最常见的报错,认出它就知道缺的是约束。

为什么返回 T[K] 而不是 any:T[K] 是索引访问类型,在每个调用点按实参单独求值 ——getProp(user, "name") 得 string,getProp(user, "active") 得 boolean。 精确性的来源是 K 被推断成字面量类型(literal type)"name",而不是 string)。 返回 any 的版本编译也过,但类型信息在函数出口全部蒸发, 调用方从此拿着一个不受检查的值。

会追问:「那写(obj: object, key: string): any 有什么问题?」—— 能跑,但传错对象、拼错键、用错返回值,全都要等运行时才炸; junior 版和 senior 版的差距就在这。 「约束能带默认值吗?」—— 能:<T extends object = Record<string, unknown>>。 「为什么要两个类型参数?」—— 因为 K 的合法取值依赖 T。 「一个参数的取值范围由另一个参数决定」, 正是泛型约束的典型使用场景。

In one line: the constraint proves to the compiler that K is one of T’s property names. Only then does obj[key] compile, and only then can the return type be written precisely as T[K] — one constraint buys both legality and precision.

What fails without it: Type 'K' cannot be used to index type 'T'. With no constraint there is no relationship between K and T, so the compiler cannot prove the key exists on obj, and it rejects the index expression outright. It is the single most common error in generics questions — recognize it and you know a constraint is missing.

Why return T[K] rather than any: T[K] is an indexed access type, evaluated per call site against the actual argument — getProp(user, "name") gives string, getProp(user, "active") gives boolean. The precision comes from K being inferred as a literal type ("name", not string). The any version compiles too, but every bit of type information evaporates at the function exit, and callers are left holding an unchecked value.

Follow-up: “So what is wrong with (obj: object, key: string): any?” — it runs, but a wrong object, a misspelled key or a misused return value all wait until runtime to fail; that gap is the junior version versus the senior one. “Can a constraint have a default?” — yes: <T extends object = Record<string, unknown>>. “Why two type parameters?” — because the legal values of K depend on T. One parameter whose range is decided by another is exactly what generic constraints are for.

TypeScript约束前 vs 约束后示意Illustrative
1// 不加约束:K 和 T 之间没有任何关系,编译器不能证明 obj 上有这个键
2function getPropBad<T, K>(obj: T, key: K) {
3 // return obj[key];
4 // ✗ Type 'K' cannot be used to index type 'T'.
5}
6
7// 加约束:K 被限制在 T 的属性名里,返回类型精确到 T[K]
8function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
9 return obj[key];
10}
11
12const user = { id: 1, name: "Ada", active: true };
13
14const n = getProp(user, "name"); // n 的类型是 string
15const a = getProp(user, "active"); // a 的类型是 boolean
16// getProp(user, "email"); // ✗ 编译期就挡住,不用等运行时的 undefined
1// Without a constraint: K and T are unrelated, so the compiler cannot prove obj has that key
2function getPropBad<T, K>(obj: T, key: K) {
3 // return obj[key];
4 // ✗ Type 'K' cannot be used to index type 'T'.
5}
6
7// With a constraint: K is limited to T's property names, and the return type is exactly T[K]
8function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
9 return obj[key];
10}
11
12const user = { id: 1, name: "Ada", active: true };
13
14const n = getProp(user, "name"); // n has type string
15const a = getProp(user, "active"); // a has type boolean
16// getProp(user, "email"); // ✗ stopped at compile time, no runtime undefined
TypeScript 深度TypeScript deep diveDrillLab 自出By DrillLab

判别联合怎么配合 switch 做穷尽检查

How do discriminated unions enable exhaustiveness checking

看答案Show answer

一句话:判别联合(discriminated union, 也译可辨识联合)指每个成员都带同一个字段、 且字段类型是互不相同的字面量。switch 这个字段, 每个 case 里编译器自动收窄;default里把值赋给 never,将来加了成员忘了处理, 编译直接失败 —— 这就是穷尽检查(exhaustiveness check)。

穷尽检查的机制:控制流走到 default 时, 编译器算出「还没被 case 排除的成员」。三个成员都处理过, 剩下的是 never,const exhausted: never = s 成立; 第四个成员加进来却没写 case,剩下的就不是 never, 这一行立刻报错。错误出现在「忘了改的那处代码」, 而不是上线之后。

对比 as 断言:收窄的每一步都对应一个 真实的运行时检查,是「证明」;as不产生任何运行时代码,是「让编译器闭嘴」。JSON.parse(...) as Shape 能过编译, 但值长什么样由运行时说了算, 错误被推迟到离出错点很远的地方爆发。 as 有正当用途(类型守卫内部、测试代码、DOM 查询的细化), 但拿它替代收窄,等于手动关掉编译期的保障。

会追问:「typeof 收窄为什么不够用?」—— typeof 对一切对象都返回 "object", 分不开对象联合的成员;判别联合就是给对象联合准备的收窄手段。 本站 foundations 的 TS 课里 SettledResultstatus 收窄就是同一招,这一题只是往深处多走一层。 「判别字段为什么必须是字面量类型?」—— 两个成员的字段如果都是 string,编译器无从区分。 还会问 assertNever:把 default 里那两行抽成function assertNever(x: never): never, 每个 switch 复用。

In one line: a discriminated union is one where every member carries the same field, each typed as a different literal. switch on that field and the compiler narrows automatically in every case; in default, assign the value to never, and if a member is added later without a case, compilation fails on the spot — that is exhaustiveness checking.

How the check works: when control flow reaches default, the compiler computes which members the cases have not eliminated. With all three handled, what remains is never, so const exhausted: never = s holds; add a fourth member without a case and what remains is no longer never, so that line errors immediately. The failure shows up at the code you forgot to change — not in production.

Contrast with as: every narrowing step corresponds to a real runtime check — it is proof. as emits no runtime code at all — it just silences the compiler. JSON.parse(...) as Shape compiles, but the runtime decides what the value actually looks like, and the error detonates far from where it was planted. as has legitimate uses (inside type guards, in test code, refining DOM queries), but using it in place of narrowing means switching off the compile-time guarantee by hand.

Follow-up: “Why is typeof narrowing not enough?” — typeof answers "object" for every object, so it cannot tell union members apart; discriminated unions are the narrowing tool built for object unions. The foundations TS lesson on this site narrows SettledResult by status with the same move — this question just goes one level deeper. “Why must the discriminant be a literal type?” — if both members type the field as string, the compiler has nothing to tell them apart by. Expect assertNever too: extract those two default lines into function assertNever(x: never): never and reuse it in every switch.

TypeScript判别联合与穷尽检查示意Illustrative
1type Shape =
2 | { kind: "circle"; radius: number }
3 | { kind: "square"; side: number }
4 | { kind: "rect"; width: number; height: number };
5
6function area(s: Shape): number {
7 switch (s.kind) {
8 case "circle":
9 return Math.PI * s.radius ** 2; // 这个分支里 s 已收窄为 circle
10 case "square":
11 return s.side ** 2;
12 case "rect":
13 return s.width * s.height;
14 default: {
15 // 穷尽检查:三个成员都处理过,s 在这里只能是 never。
16 // 将来加了第四种 kind 却忘了写 case,这一行立刻编译报错。
17 const exhausted: never = s;
18 return exhausted;
19 }
20 }
21}
1type Shape =
2 | { kind: "circle"; radius: number }
3 | { kind: "square"; side: number }
4 | { kind: "rect"; width: number; height: number };
5
6function area(s: Shape): number {
7 switch (s.kind) {
8 case "circle":
9 return Math.PI * s.radius ** 2; // inside this branch s is narrowed to circle
10 case "square":
11 return s.side ** 2;
12 case "rect":
13 return s.width * s.height;
14 default: {
15 // Exhaustiveness check: all three members are handled, so s can only be never here.
16 // Add a fourth kind and forget its case, and this line fails to compile at once.
17 const exhausted: never = s;
18 return exhausted;
19 }
20 }
21}
TypeScriptas 是闭嘴,收窄是证明示意Illustrative
1// as 是「让编译器闭嘴」,不是「向编译器证明」
2const draft = JSON.parse(localStorage.getItem("draft") ?? "{}") as Shape;
3// 编译器信了。但运行时这里可能是任何东西,错误被推迟到别处爆发
4
5// 类型守卫是「证明」:每一步收窄都有真实的运行时检查兜着
6function isShape(x: unknown): x is Shape {
7 if (typeof x !== "object" || x === null) return false;
8 if (!("kind" in x)) return false;
9 return x.kind === "circle" || x.kind === "square" || x.kind === "rect";
10}
11
12const raw: unknown = JSON.parse(localStorage.getItem("draft") ?? "{}");
13if (isShape(raw)) {
14 area(raw); // ✓ 这一行的安全是运行时检查换来的,不是宣称出来的
15}
1// as tells the compiler to be quiet; it does not prove anything to the compiler
2const draft = JSON.parse(localStorage.getItem("draft") ?? "{}") as Shape;
3// The compiler believes it. At runtime this can be anything, and the error surfaces elsewhere
4
5// A type guard is a proof: every narrowing step is backed by a real runtime check
6function isShape(x: unknown): x is Shape {
7 if (typeof x !== "object" || x === null) return false;
8 if (!("kind" in x)) return false;
9 return x.kind === "circle" || x.kind === "square" || x.kind === "rect";
10}
11
12const raw: unknown = JSON.parse(localStorage.getItem("draft") ?? "{}");
13if (isShape(raw)) {
14 area(raw); // ✓ this line is safe because of the runtime check, not because we said so
15}
TypeScript 深度TypeScript deep diveDrillLab 自出By DrillLab

unknown、any、never 各自是什么语义

What do unknown, any and never each mean

看答案Show answer

一句话:unknown 是 「还不知道是什么,用之前必须先证明」;any 是 「放弃检查,双向放行」;never 是「不可能有值」。 三个词各占类型系统的一个极端。

放进类型层级看:

  • unknown 是顶类型(top type):任何值都能赋给它; 但不先收窄,它什么都做不了 —— 连u.toUpperCase() 都编译不过。
  • never 是底类型(bottom type):它能赋给任何类型, 但没有类型能赋给它 —— 因为它根本没有值。
  • any 不在层级里,它是关掉检查的开关: 双向都能赋,而且会传染 —— 碰过 any 的表达式结果还是 any, 一处 any 能顺着数据流污染一整个模块。

catch (e) 怎么处理:strict 模式下 (TS 4.4 起的 useUnknownInCatchVariables) e 是 unknown,因为 JS 允许 throw 任何值。直接读e.message 编译不过;标准写法是e instanceof Error 收窄后读 message, else 分支 String(e) 兜底。推而广之: JSON.parse 的结果、API 响应、一切外部输入, 入口处都该标 unknown,收窄之后再进业务代码 —— unknown 是边界上的类型。

会追问:「void 和 never 什么区别?」—— void 是正常返回、只是不带值;never 是根本不会正常返回 (throw 或死循环)。「never 还有什么用?」—— 上一题的穷尽检查,加上在 Exclude 里当删除用: never 是空联合,并进联合就消失。 「为什么宁用 unknown 不用 any?」—— unknown 把「先检查再用」变成编译器强制的动作; any 把它变成自觉,而自觉在赶工期的时候最先消失。

In one line: unknown means “not known yet — prove it before you use it”; any means “checking abandoned, both directions waved through”; never means “no value can exist here”. Three words, three extremes of the type system.

Placed in the type hierarchy:

  • unknown is the top type: every value is assignable to it; but until you narrow it, it can do nothing — even u.toUpperCase() refuses to compile.
  • never is the bottom type: it is assignable to everything, yet nothing is assignable to it — because it has no values at all.
  • any sits outside the hierarchy; it is the switch that turns checking off: assignable both ways, and contagious — an expression that touches any becomes any, and one any can pollute a whole module along the data flow.

Handling catch (e): under strict mode (useUnknownInCatchVariables, since TS 4.4) e is unknown, because JS lets you throw anything. Reading e.message directly does not compile; the standard shape is narrowing with e instanceof Error before touching message, with String(e) as the else fallback. Generalize it: the result of JSON.parse, API responses, all external input should enter as unknown and get narrowed before reaching business code — unknown is the type for boundaries.

Follow-up: “void versus never?” — void returns normally, just without a value; never does not return normally at all (it throws, or loops forever). “What else is never for?” — the exhaustiveness check from the previous card, plus playing deletion inside Exclude: never is the empty union, so merging it in makes members disappear. “Why unknown over any?” — unknown turns check-before-use into something the compiler enforces; any turns it into self-discipline, and self-discipline is the first thing to go when a deadline lands.

TypeScript三个极端示意Illustrative
1// any:双向放行 —— 什么都能赋给它,它也能赋给任何类型
2const a: any = JSON.parse('"hi"');
3const n1: number = a; // 编译器不吭声,n1 实际是个字符串
4a.toFixed(); // 编译通过,运行时 TypeError
5
6// unknown:进来随便,出去必须先收窄
7const u: unknown = JSON.parse('"hi"');
8// const n2: number = u; // ✗ Type 'unknown' is not assignable to type 'number'
9// u.toUpperCase(); // ✗ 'u' is of type 'unknown'
10if (typeof u === "string") {
11 u.toUpperCase(); // ✓ 证明它是 string 之后才能用
12}
13
14// never:不可能有值 —— 要么抛错,要么根本走不到
15function fail(msg: string): never {
16 throw new Error(msg);
17}
1// any lets everything through both ways: anything goes into it, and it goes into anything
2const a: any = JSON.parse('"hi"');
3const n1: number = a; // the compiler says nothing, and n1 is really a string
4a.toFixed(); // compiles, then throws a TypeError at runtime
5
6// unknown: anything comes in, but you must narrow it before it goes out
7const u: unknown = JSON.parse('"hi"');
8// const n2: number = u; // ✗ Type 'unknown' is not assignable to type 'number'
9// u.toUpperCase(); // ✗ 'u' is of type 'unknown'
10if (typeof u === "string") {
11 u.toUpperCase(); // ✓ usable once it is proved to be a string
12}
13
14// never: no value is possible — it either throws or is unreachable
15function fail(msg: string): never {
16 throw new Error(msg);
17}
TypeScriptcatch (e) 的标准处理示意Illustrative
1try {
2 JSON.parse("{oops");
3} catch (e) {
4 // strict 下(useUnknownInCatchVariables)e 是 unknown:
5 // console.error(e.message); // ✗ 'e' is of type 'unknown'
6 if (e instanceof Error) {
7 console.error(e.message); // ✓ 收窄成 Error 之后才能读 message
8 } else {
9 console.error(String(e)); // 兜底:JS 允许 throw 任何值,包括字符串
10 }
11}
1try {
2 JSON.parse("{oops");
3} catch (e) {
4 // Under strict (useUnknownInCatchVariables) e is unknown:
5 // console.error(e.message); // ✗ 'e' is of type 'unknown'
6 if (e instanceof Error) {
7 console.error(e.message); // ✓ readable only after narrowing to Error
8 } else {
9 console.error(String(e)); // fallback: JS allows throwing any value, strings included
10 }
11}

这些题从哪来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.