泛型与收窄:把 any 赶出代码Generics and narrowing: getting any out of the code
getProp 为什么必须约束 K extends keyof T,判别联合加 never 兜底做穷尽检查,unknown / any / never 的三种语义。Why getProp must constrain K extends keyof T, exhaustiveness checking with a discriminated union and a never fallback, and what unknown / any / never each mean.
这一页有什么On this page5
- 写出 getProp<T, K extends keyof T>(obj: T, key: K): T[K],并解释约束和返回类型各解决什么Write getProp<T, K extends keyof T>(obj: T, key: K): T[K], and explain what the constraint and the return type each fix
- 用判别联合、switch 收窄和 never 兜底写出编译期的穷尽检查Use a discriminated union, a switch that narrows, and a never fallback to get an exhaustiveness check at compile time
- 说清 as 断言为什么是逃生舱:它让编译器闭嘴,不产生任何运行时检查Explain why an as assertion is only an escape hatch: it silences the compiler and adds no runtime check
- 分清 unknown / any / never,并写出 catch (e) 的标准处理Tell unknown / any / never apart, and write the standard handling for catch (e)
senior 面试的泛型题多半长成 getProp 的样子:先让你写,再追问「不写约束行不行」「返回 any 行不行」。收窄和 unknown 两道验的是同一件事 —— 不靠 any 也能过编译。代码里 any 的密度,面试官是真的会看。Generic questions in senior interviews usually look like getProp: first write it, then answer what happens without the constraint, and what happens if it returns any. The narrowing question and the unknown question test the same thing — your code compiles without any. Interviewers really do look at how much any is in your code.
为什么 getProp 必须写 K extends keyof TWhy does getProp need K extends keyof T?
Why does getProp need the constraint K extends keyof T
一句话:这个约束向编译器证明 「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.
判别联合怎么配合 switch 做穷尽检查How does a discriminated union work with switch to check every case?
How do discriminated unions enable exhaustiveness checking
一句话:判别联合(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 课里 SettledResult 按status 收窄就是同一招,这一题只是往深处多走一层。 「判别字段为什么必须是字面量类型?」—— 两个成员的字段如果都是 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.
unknown、any、never 各自是什么语义What do unknown, any and never each mean?
What do unknown, any and never each mean
一句话: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:
unknownis the top type: every value is assignable to it; but until you narrow it, it can do nothing — evenu.toUpperCase()refuses to compile.neveris the bottom type: it is assignable to everything, yet nothing is assignable to it — because it has no values at all.anysits 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.
动手做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.
这个函数编译不过:'e' is of type 'unknown'.下面哪种改法是对的?
This function does not compile: 'e' is of type 'unknown'. Which fix is the right one?
换一道题也能用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.
- 泛型约束一举两得:obj[key] 合法化,返回类型精确到 T[K]。The generic constraint does two things at once: obj[key] becomes legal, and the return type is exactly T[K].
- T[K] 的精确来自 K 被推断成字面量类型,而不是 string。T[K] is exact because K is inferred as a literal type, not as string.
- 判别联合 = 同名字段、不同字面量;switch 收窄,default 赋给 never 做穷尽检查。A discriminated union has one field with the same name in every member, holding a different literal; switch narrows on it, and assigning to never in the default branch checks that no case is left out.
- as 是闭嘴不是证明:不产生运行时检查,错误被推迟到别处爆发。as silences the compiler, it does not prove anything: there is no runtime check, so the error surfaces later somewhere else.
- unknown 是顶、never 是底、any 在层级外还会传染;边界一律 unknown。unknown is the top type, never is the bottom type, and any sits outside the hierarchy and spreads; use unknown at every boundary.
- catch (e) 的 e 是 unknown:instanceof Error 收窄,String(e) 兜底。In catch (e), e is unknown: narrow it with instanceof Error, and fall back to String(e).