Chapter 03

自测与辨析

第 1 章给了类型系统的零件,第 2 章讲了它们背后的设计取舍。这一章不再喂新知识,而是把它们逼出来——三层梯度题 + 跨章判别场景 + 默画概念图。读得顺不算数,能在合上教程后答对才算。

使用方式 · 对抗流畅幻觉

所有答案集中折叠在页面最底部一个块里,刻意离题目很远。先把每道题的答案写在纸上或编辑器里,全部做完,再翻到底部对照。中途瞄答案,等于把题目当正文又读了一遍——那正是导览页警告过的「我读得很顺」假象。

概念层 · 回忆 / 理解 对应第 1 章 · 8 题 原理层 · 理解 / 应用 对应第 2 章 · 7 题 应用判别层 跨章 · 5 题 难 易
图 3.1自测的三层梯度。注意:越往上,题目越不能靠「背过」答出,越需要把第 1、2 章的概念组合起来现场推——顶层的判别题就是用来戳破「我读得顺」却「迁移不动」的幻觉的。

一概念层(对应第 1 章)

  1. 类型注解和类型推断的区别是什么?说出至少两处「必须手写注解」的场景。提示:§1.3
  2. TS 的 number 和 Java 的数字类型有什么本质不同?举一个因此会出问题的具体例子。提示:§1.2
  3. null 和 undefined 在语义上分别表示什么?为什么用 === null 判空有风险?提示:§1.2
  4. 用「值的集合」分别描述 unknown、any、never。哪个不是真正的集合?提示:§1.2 + 图 1.1
  5. interface 和 type,各自只有对方做不到的一件事是什么?提示:§1.4 表 1.1
  6. 把 this 丢失的那个回调例子,说清「为什么丢」和「两种修复」。提示:§1.6
  7. 「收窄(narrowing)」是什么?列出至少三种收窄手段。提示:§1.8
  8. 为什么泛型函数里写不了 new T() 或 x instanceof T?这和 Java 是同一个原因吗?提示:§1.7

二原理层(对应第 2 章)

  1. 一句话说清「结构化类型」和「名义类型」的区别。提示:§2.1
  2. 「多余属性检查」是什么?为什么它让「直接赋值」和「经过中间变量」表现不同?提示:§2.1
  3. 描述 tsc 的编译流水线,指出类型在哪一步消失。提示:§2.2 图 2.2
  4. tsx / Node 原生跑 TS,和 tsc 的本质区别是什么?为什么还要 tsc --noEmit?提示:§2.2 表 2.2
  5. 「TS 不健全是故意的」——这话什么意思?举两个逃生舱。提示:§2.3
  6. enum 为什么是「擦除的例外」?它编译成什么?提示:§2.3 图 2.3
  7. strict 是什么级别的开关?strictNullChecks 关闭和打开各是什么后果?提示:§2.4

三应用判别层(综合第 1、2 章 · 这层最难)

每题给一个场景,要你选一个方案并说明为什么——答案往往横跨两章。这是检验「真懂」还是「读着顺」的分水岭。

  1. interface 还是 type?你要为一个函数的返回值建模,它是 { kind: "ok"; data: string } | { kind: "err"; message: string } 这样的联合。选哪个声明方式?换成给一个普通的 User 对象建模、且希望第三方能扩展它,又选哪个?
  2. any 还是 unknown?你调用一个没有类型声明的老 JS 库,它的返回值形状不确定。先用什么类型接住它最稳?接住之后要做什么才能安全使用?
  3. enum 还是字面量联合?定义一个 HTTP 方法集合 GET/POST/PUT/DELETE,要频繁和 JSON、请求头里的字符串互转。选哪个?从「运行时开销」和「与字符串的兼容性」两个角度说理由。
  4. 怎么区分两个对象?有 type Dog = { bark(): void } 和 type Cat = { meow(): void },参数是 Dog | Cat。能不能用 animal instanceof Dog 区分?为什么?应该用什么手段?(这题同时踩 §1.8 和 §2.2)
  5. as 还是 zod?一个 webhook 推来一段 JSON,你要把它当作 { event: string; amount: number } 使用。用 as 断言,还是用 zod 校验?从「类型擦除」和「不健全」两个原理说清,为什么 as 在这里是个定时炸弹。(这题同时踩 §2.2 和 §2.3)
亲手画一张图

合上教程,在纸上默画导览页那张「两个世界」概念图——只画两个框(编译期类型世界 / 运行时值世界)+ 中间的 tsc 擦除边界,每个框里填 3 个零件。

画完回到 导览页图 0 对照,重点检查三件事:① 你有没有把 泛型放在「类型世界」那侧(它运行时被擦除)?② 你有没有把 enum 放在「值世界」那侧(它是唯一会漏过去的例外)?③ 你画的 narrowing 在哪侧——它其实是连接两侧的桥。哪个放错了,就回对应小节再看一遍。

§答案(三层全部做完再展开)

最后的检验:合上教程,三层 20 题独立作答完毕,再展开这里。

展开全部答案

一 · 概念层

  1. 注解是你手写给值的类型;推断是 TS 根据值自动算出的类型。必须手写的场景:函数参数、对外导出的 API 返回值(不应依赖推断)、外部数据入口。其余局部变量交给推断。
  2. TS 只有一个 number(IEEE 754 浮点),没有 int/long/double 之分。例子:10 / 3 得 3.333… 而非整数 3;0.1 + 0.2 !== 0.3;数组越界返回 undefined 而不抛异常。
  3. undefined = 没赋值 / 不存在(自然发生的空);null = 显式赋的空。风险:null === undefined 为 false,只判 === null 会漏掉 undefined 的情况。
  4. unknown = 全集(所有值),是安全顶类型,用前必须收窄;never = 空集(无任何值);any 不是真正的集合,它是「关闭类型检查」的开关。
  5. interface 独有:同名声明自动合并(declaration merging)。type 独有:能给联合 / 元组 / 原始类型起别名、做类型运算。
  6. 方法被单独取出当回调时,与原对象的绑定断了,调用时 this 不再是该对象(严格模式下为 undefined),this.x 抛错。两种修复:① 用箭头函数(词法绑定 this);② obj.method.bind(obj)。
  7. 收窄 = 用运行时能做的值检查,让 TS 把宽类型缩到具体类型。手段:typeof、instanceof(仅 class)、in、比较字面量 / 带标签的联合(discriminated union)、真值判断。
  8. 因为 T 是编译期类型参数,运行时被擦除、根本不存在,没有东西供 new 或 instanceof 使用。和 Java 同源(Java 泛型也擦除),但 TS 更彻底——连原始类型都不保留。

二 · 原理层

  1. 结构化:看形状(有没有要求的成员)判定兼容;名义:看声明的名字 + 继承链判定兼容。TS 是前者,Java 是后者。
  2. 多余属性检查:把新鲜的对象字面量直接赋给某类型时,多出未声明的属性会报错。它是结构化规则之外的一个补丁,只对字面量直接赋值生效;经过中间变量后走纯结构化规则(只要求「至少有」要求的成员),多余属性被忽略,于是不报错。
  3. 流水线:.ts 源码(值+类型) → tsc 类型检查 → 擦除 + 生成 → 纯 JS → 运行。类型在「擦除 + 生成」这一步消失。
  4. tsx/Node 原生只剥离类型、不做类型检查,图快;tsc 会检查。所以光用它们跑,类型错误不被发现,需要在 CI 用 tsc --noEmit 单独把关(只查不生成)。
  5. 意思是 TS 明知有些会在运行时崩的程序也放行——它选择「实用 > 可证明正确」,用逃生舱兼容存量 JS。两个逃生舱:any(关闭检查)、as 断言(覆盖检查、运行时零验证)。(数组协变也算。)
  6. 因为 enum 会编译成一个真实存在的 JS 对象(数字 enum 还带正向 + 反向双映射),留在运行时——这与「类型几乎全被擦除」相反,故是例外。字面量联合则零运行时开销。
  7. strict 是元开关,一次打开约 8 个子检查。strictNullChecks 关闭时 null/undefined 可赋给任何类型、NPE 留到运行时;打开后空值必须显式处理(如写成 string | null),编译期就拦下。

三 · 应用判别层

  1. 联合返回值用 type(interface 描述不了联合)。普通 User 对象、且要第三方可扩展,用 interface(可被 extends,也支持声明合并来增补)。判别点:要不要表达「联合 / 元组」→ 只能 type;要不要「可扩展的对象契约」→ 倾向 interface。
  2. 先用 unknown 接住(不是 any——any 会让后续所有访问失去检查、错误漏到运行时)。接住后必须先收窄(typeof / in / 逐字段检查,或直接用 zod 校验)才能安全使用。
  3. 用字面量联合 type Method = "GET" | "POST" | "PUT" | "DELETE"。理由:① 零运行时开销(纯类型、被擦除),enum 会生成运行时对象;② 它的值本身就是字符串,和 JSON / 请求头天然互通,不需要 enum↔字符串转换。
  4. 不能用 instanceof。因为 Dog/Cat 是 type(接口形状),运行时被擦除、不存在构造函数供 instanceof 检查(§2.2 擦除)。应改用 in("bark" in animal)或把它们改造成带标签的联合(加 kind 字段)后按字段收窄(§1.8)。
  5. 用 zod。原理:类型擦除(§2.2)→ 运行时没有类型,webhook 的 JSON 不受类型系统保护;as 不健全(§2.3)→ 它只在编译期骗过检查、运行时零验证。所以 as { event; amount } 在字段缺失 / 类型不符时不会报错,等到你用 amount.toFixed() 之类才在远处炸 TypeError——定时炸弹。zod 在数据入口当场验,且能反推静态类型。
洞察 · 一条线收束全篇

如果三层里你卡住的题,几乎都能追溯到同一句话——「类型只在编译期、运行时被擦除;且按结构而非名字匹配」——那这份教程的目标就达成了。as 不安全、要用 zod、enum 特殊、泛型运行时为空、instanceof 不能用于 interface,全是这一条的推论,而不是五条要分别背的规则。