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.

题目Questions

筛出 105 道 · 第 9 / 9 页。105 of 105 questions · page 9 / 9.
网络与安全Web & security#361

session vs cookie

sessions vs cookies

看答案Show answer

先纠正一个常见混淆:它们不是同级的东西。cookie 是「浏览器存小数据的机制」session 是「服务器记住用户状态的方案」—— 而 session 通常靠 cookie 来传那个 id

CookieSession
存在哪浏览器服务器(内存 / Redis / 数据库)
存什么小字符串(≤ 4 KB)任意大小的用户数据
安全性用户能看能改用户只拿到一个 id
能否主动失效要等过期或被覆盖能,删掉服务端记录就行

典型流程:登录成功 → 服务器建 session、 生成 session id → 通过 Set-Cookie 发给浏览器 → 之后每个请求浏览器自动带上 → 服务器用 id 查出用户。

Cookie 的四个安全属性必须会:

  • HttpOnly—— JS 读不到,防 XSS 偷 cookie
  • Secure—— 只在 HTTPS 下发送
  • SameSite——Strict / Lax /None防 CSRF的主要手段
  • Max-Age / Domain /Path—— 作用范围

会追问:「session vs JWT 怎么选?」——

  • 要能立刻踢人下线(后台管理、支付类)→ session
  • 多服务、跨域、 移动端 + Web 共用JWT(配 refresh token)

还会问:「session 在多实例部署下怎么办?」—— 存内存会导致「刷新一下就掉登录」 (请求打到别的实例)。 解法是把 session 存 Redis, 或者用粘性会话(不推荐)。

First, clear up the usual confusion: these are not two options at the same level. A cookie is a browser mechanism for storing a small value; a session is a server-side way of remembering who the user is — and a session normally uses a cookie to carry its id.

CookieSession
Lives whereThe browserThe server (memory / Redis / database)
Holds whatA small string, 4 KB or lessUser data of any size
SecurityThe user can read it and change itThe user only ever holds an id
Can you revoke it?Only by expiry or overwriteYes — delete the server-side record

The typical flow: login succeeds → the server creates a session and a session id → it goes out via Set-Cookie → the browser attaches it to every subsequent request → the server looks the user up by that id.

You have to know the four cookie security attributes:

  • HttpOnly — JS cannot read it, which stops XSS from stealing the cookie
  • Secure — only sent over HTTPS
  • SameSite Strict / Lax / None, the main defence against CSRF
  • Max-Age / Domain / Path — its scope

Follow-up: “Session or JWT?” —

  • You need to kick someone out right now (admin panels, anything touching payments) → session
  • Many services, cross-domain, one API for both mobile and web JWT, with a refresh token

They will also ask: “What happens to sessions across several instances?” — keeping them in memory means “refresh the page and I am logged out” when the request lands on a different instance. The fix is to put sessions in Redis, or sticky sessions (not recommended).

网络与安全Web & security#362

常见的 HTTP 状态码

Give some HTTP response status codes

看答案Show answer

先说五个类别,再举例 —— 这样显得有体系:1xx 信息、2xx 成功3xx 重定向4xx 客户端错5xx 服务端错

含义什么时候用
200 OK成功GET / PUT / PATCH 成功
201 Created已创建POST 成功,建议带 Location 头
204 No Content成功但没内容DELETE 成功
301 / 302永久 / 临时重定向301 会被浏览器缓存,改错了很难收回
304 Not Modified没变,用缓存配 ETag / Last-Modified
400 Bad Request请求有问题参数缺失、格式错、校验失败
401 Unauthorized没登录 / token 无效「你是谁?」
403 Forbidden登录了但没权限「知道你是谁,但你不能干这个」
404 Not Found资源不存在
409 Conflict冲突重复注册、并发修改
422语义错误格式对但业务上不合法
429请求太多限流
500服务端异常未捕获的错误
502 / 503 / 504网关错 / 不可用 / 超时上游挂了、在维护、上游太慢

401 vs 403 是最常问的一对401 是「没认证」,403 是「认证了但没授权」
实践里有个细节:为了不泄露资源是否存在, 有些接口会把「没权限」也返回 404。

会追问:「业务错误该用 4xx 还是 200 带错误码?」——REST 风格用 4xx(让 HTTP 语义承载错误), 但要注意有些老网关会吞掉 4xx 的响应体GraphQL 则一律返 200, 错误放在 errors 字段里 —— 因为一个请求可能部分成功, 没法用单个状态码表达。这个对比答出来很加分, Federation 那门课里 extensions.code就是干这个的。

Name the five classes first, then give examples — it reads as organised: 1xx informational, 2xx success, 3xx redirect, 4xx client error, 5xx server error.

CodeMeansWhen
200 OKSuccessA successful GET / PUT / PATCH
201 CreatedCreatedA successful POST — send a Location header
204 No ContentSuccess, nothing to returnA successful DELETE
301 / 302Permanent / temporary redirectBrowsers cache 301, so a wrong one is hard to take back
304 Not ModifiedUnchanged, use your cachePaired with ETag or Last-Modified
400 Bad RequestThe request is wrongMissing parameter, bad format, failed validation
401 UnauthorizedNot logged in, or the token is invalid“Who are you?”
403 ForbiddenLogged in but not allowed“I know who you are, and you cannot do this”
404 Not FoundNo such resource
409 ConflictConflictDuplicate signup, concurrent edit
422Semantically wrongWell-formed but invalid for the business rules
429Too many requestsRate limiting
500Server failedAn uncaught error
502 / 503 / 504Bad gateway / unavailable / timeoutUpstream is down, in maintenance, or too slow

401 vs 403 is the pair they ask about most: 401 means not authenticated, 403 means authenticated but not authorised.
One detail from practice: to avoid leaking whether a resource exists, some endpoints return 404 for “not allowed” as well.

Follow-up: “Should a business error be a 4xx or a 200 with an error code?” — REST says 4xx, so the HTTP semantics carry the error, but watch out: some older gateways swallow the body of a 4xx. GraphQL always returns 200 and puts errors in the errors field, because one request can be partially successful and no single status code says that. Drawing that contrast earns you points — in the Federation course, extensions.code is exactly this.

网络与安全Web & security#357

测试有哪几种

What are the different kinds of tests

看答案Show answer

一句话:按范围从小到大 ——单元 → 集成 → 端到端, 这就是「测试金字塔」:越往上越慢越脆,所以数量越少

  • 单元测试(unit)—— 测一个函数或一个组件, 依赖全部 mock。快、多、定位准。例:一个纯函数、 一个 React 组件的渲染。
  • 集成测试(integration)—— 测几个模块协作是否正确, 可能真的连数据库或起一个测试服务器。 例:调一个 API 端点, 断言它真的写进了库。
  • 端到端(E2E)——用真实浏览器走完整用户流程。 Playwright / Cypress。 最接近真实,也最慢最容易随机失败。

还会提到的几种:回归测试(防止改坏老功能)、 快照测试(比对渲染输出,容易变成「随手更新快照」的橡皮章)、 性能 / 压力测试、 可访问性测试、 冒烟测试(上线后快速验证主流程)。

Testing Library 的核心理念值得说:「像用户一样测试」—— 按可见文本和 role 查元素, 而不是按 class 名或组件内部结构。 这样重构内部实现测试不会碎。

会追问:「测试覆盖率要多少?」——不要给一个死数字。 正确回答是:覆盖率只说明「代码被执行过」, 不说明「断言是对的」
这一点我可以给一个实测例子: Federation 那门课的源项目里, 六个端点全部 return null也能通过 3 个测试, node-subgraph 的 4 个「通过」里3 个是「空实现恰好满足断言」所以「测试通过 ≠ 做对了」—— 比覆盖率数字更该关心的是断言够不够强。

In one line: smallest scope to largest — unit → integration → end-to-end. That is the testing pyramid: higher means slower and flakier, so you write fewer of them.

  • Unit — one function or one component, everything else mocked. Fast, numerous, and precise about where the problem is. A pure function; a React component rendering.
  • Integration — do a few modules work together, possibly against a real database or a test server. Call an API endpoint and assert the row really landed.
  • End-to-end a real browser walking a whole user journey. Playwright or Cypress. Closest to reality, and also the slowest and the most prone to random failure.

Others worth mentioning: regression tests (so old behaviour does not break), snapshot tests (comparing rendered output — which easily degrades into rubber-stamping “update snapshot”), performance and load tests, accessibility tests, and smoke tests for a quick check of the main flow after a deploy.

Testing Library’s core idea is worth stating: “test it the way a user uses it” — find elements by visible text and role, not by class name or internal component structure. Then refactoring the internals does not shatter the tests.

Follow-up: “What coverage number should you aim for?” — do not give a number. The right answer is that coverage tells you code was executed, not that the assertions are any good.
Here is a measured example: in the source project behind the Federation course, six endpoints that all just return null still passed 3 tests, and of the 4 passes in node-subgraph, 3 were an empty implementation happening to satisfy the assertion. So a green test does not mean you got it right — the strength of the assertions matters more than the coverage percentage.

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
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.