Chapter 03
自测与辨析
第 1 章给了类型系统的零件,第 2 章讲了它们背后的设计取舍。这一章不再喂新知识,而是把它们逼出来——三层梯度题 + 跨章判别场景 + 默画概念图。读得顺不算数,能在合上教程后答对才算。
使用方式 · 对抗流畅幻觉
所有答案集中折叠在页面最底部一个块里,刻意离题目很远。先把每道题的答案写在纸上或编辑器里,全部做完,再翻到底部对照。中途瞄答案,等于把题目当正文又读了一遍——那正是导览页警告过的「我读得很顺」假象。
一概念层(对应第 1 章)
- 类型注解和类型推断的区别是什么?说出至少两处「必须手写注解」的场景。提示:§1.3
- TS 的
number和 Java 的数字类型有什么本质不同?举一个因此会出问题的具体例子。提示:§1.2 null和undefined在语义上分别表示什么?为什么用=== null判空有风险?提示:§1.2- 用「值的集合」分别描述
unknown、any、never。哪个不是真正的集合?提示:§1.2 + 图 1.1 interface和type,各自只有对方做不到的一件事是什么?提示:§1.4 表 1.1- 把
this丢失的那个回调例子,说清「为什么丢」和「两种修复」。提示:§1.6 - 「收窄(narrowing)」是什么?列出至少三种收窄手段。提示:§1.8
- 为什么泛型函数里写不了
new T()或x instanceof T?这和 Java 是同一个原因吗?提示:§1.7
二原理层(对应第 2 章)
- 一句话说清「结构化类型」和「名义类型」的区别。提示:§2.1
- 「多余属性检查」是什么?为什么它让「直接赋值」和「经过中间变量」表现不同?提示:§2.1
- 描述
tsc的编译流水线,指出类型在哪一步消失。提示:§2.2 图 2.2 tsx/ Node 原生跑 TS,和tsc的本质区别是什么?为什么还要tsc --noEmit?提示:§2.2 表 2.2- 「TS 不健全是故意的」——这话什么意思?举两个逃生舱。提示:§2.3
enum为什么是「擦除的例外」?它编译成什么?提示:§2.3 图 2.3strict是什么级别的开关?strictNullChecks关闭和打开各是什么后果?提示:§2.4
三应用判别层(综合第 1、2 章 · 这层最难)
每题给一个场景,要你选一个方案并说明为什么——答案往往横跨两章。这是检验「真懂」还是「读着顺」的分水岭。
- interface 还是 type?你要为一个函数的返回值建模,它是
{ kind: "ok"; data: string } | { kind: "err"; message: string }这样的联合。选哪个声明方式?换成给一个普通的User对象建模、且希望第三方能扩展它,又选哪个? - any 还是 unknown?你调用一个没有类型声明的老 JS 库,它的返回值形状不确定。先用什么类型接住它最稳?接住之后要做什么才能安全使用?
- enum 还是字面量联合?定义一个 HTTP 方法集合
GET/POST/PUT/DELETE,要频繁和 JSON、请求头里的字符串互转。选哪个?从「运行时开销」和「与字符串的兼容性」两个角度说理由。 - 怎么区分两个对象?有
type Dog = { bark(): void }和type Cat = { meow(): void },参数是Dog | Cat。能不能用animal instanceof Dog区分?为什么?应该用什么手段?(这题同时踩 §1.8 和 §2.2) - as 还是 zod?一个 webhook 推来一段 JSON,你要把它当作
{ event: string; amount: number }使用。用as断言,还是用 zod 校验?从「类型擦除」和「不健全」两个原理说清,为什么as在这里是个定时炸弹。(这题同时踩 §2.2 和 §2.3)
亲手画一张图
合上教程,在纸上默画导览页那张「两个世界」概念图——只画两个框(编译期类型世界 / 运行时值世界)+ 中间的 tsc 擦除边界,每个框里填 3 个零件。
画完回到 导览页图 0 对照,重点检查三件事:① 你有没有把 泛型放在「类型世界」那侧(它运行时被擦除)?② 你有没有把 enum 放在「值世界」那侧(它是唯一会漏过去的例外)?③ 你画的 narrowing 在哪侧——它其实是连接两侧的桥。哪个放错了,就回对应小节再看一遍。
§答案(三层全部做完再展开)
最后的检验:合上教程,三层 20 题独立作答完毕,再展开这里。
展开全部答案
一 · 概念层
- 注解是你手写给值的类型;推断是 TS 根据值自动算出的类型。必须手写的场景:函数参数、对外导出的 API 返回值(不应依赖推断)、外部数据入口。其余局部变量交给推断。
- TS 只有一个
number(IEEE 754 浮点),没有 int/long/double 之分。例子:10 / 3得3.333…而非整数3;0.1 + 0.2 !== 0.3;数组越界返回undefined而不抛异常。 undefined= 没赋值 / 不存在(自然发生的空);null= 显式赋的空。风险:null === undefined为false,只判=== null会漏掉undefined的情况。unknown= 全集(所有值),是安全顶类型,用前必须收窄;never= 空集(无任何值);any不是真正的集合,它是「关闭类型检查」的开关。interface独有:同名声明自动合并(declaration merging)。type独有:能给联合 / 元组 / 原始类型起别名、做类型运算。- 方法被单独取出当回调时,与原对象的绑定断了,调用时
this不再是该对象(严格模式下为undefined),this.x抛错。两种修复:① 用箭头函数(词法绑定 this);②obj.method.bind(obj)。 - 收窄 = 用运行时能做的值检查,让 TS 把宽类型缩到具体类型。手段:
typeof、instanceof(仅 class)、in、比较字面量 / 带标签的联合(discriminated union)、真值判断。 - 因为
T是编译期类型参数,运行时被擦除、根本不存在,没有东西供new或instanceof使用。和 Java 同源(Java 泛型也擦除),但 TS 更彻底——连原始类型都不保留。
二 · 原理层
- 结构化:看形状(有没有要求的成员)判定兼容;名义:看声明的名字 + 继承链判定兼容。TS 是前者,Java 是后者。
- 多余属性检查:把新鲜的对象字面量直接赋给某类型时,多出未声明的属性会报错。它是结构化规则之外的一个补丁,只对字面量直接赋值生效;经过中间变量后走纯结构化规则(只要求「至少有」要求的成员),多余属性被忽略,于是不报错。
- 流水线:
.ts 源码(值+类型)→tsc 类型检查→擦除 + 生成→纯 JS→ 运行。类型在「擦除 + 生成」这一步消失。 tsx/Node 原生只剥离类型、不做类型检查,图快;tsc会检查。所以光用它们跑,类型错误不被发现,需要在 CI 用tsc --noEmit单独把关(只查不生成)。- 意思是 TS 明知有些会在运行时崩的程序也放行——它选择「实用 > 可证明正确」,用逃生舱兼容存量 JS。两个逃生舱:
any(关闭检查)、as断言(覆盖检查、运行时零验证)。(数组协变也算。) - 因为
enum会编译成一个真实存在的 JS 对象(数字 enum 还带正向 + 反向双映射),留在运行时——这与「类型几乎全被擦除」相反,故是例外。字面量联合则零运行时开销。 strict是元开关,一次打开约 8 个子检查。strictNullChecks关闭时null/undefined可赋给任何类型、NPE 留到运行时;打开后空值必须显式处理(如写成string | null),编译期就拦下。
三 · 应用判别层
- 联合返回值用
type(interface描述不了联合)。普通User对象、且要第三方可扩展,用interface(可被extends,也支持声明合并来增补)。判别点:要不要表达「联合 / 元组」→ 只能type;要不要「可扩展的对象契约」→ 倾向interface。 - 先用
unknown接住(不是any——any会让后续所有访问失去检查、错误漏到运行时)。接住后必须先收窄(typeof/in/ 逐字段检查,或直接用 zod 校验)才能安全使用。 - 用字面量联合
type Method = "GET" | "POST" | "PUT" | "DELETE"。理由:① 零运行时开销(纯类型、被擦除),enum会生成运行时对象;② 它的值本身就是字符串,和 JSON / 请求头天然互通,不需要 enum↔字符串转换。 - 不能用
instanceof。因为Dog/Cat是type(接口形状),运行时被擦除、不存在构造函数供instanceof检查(§2.2 擦除)。应改用in("bark" in animal)或把它们改造成带标签的联合(加kind字段)后按字段收窄(§1.8)。 - 用 zod。原理:类型擦除(§2.2)→ 运行时没有类型,webhook 的 JSON 不受类型系统保护;
as不健全(§2.3)→ 它只在编译期骗过检查、运行时零验证。所以as { event; amount }在字段缺失 / 类型不符时不会报错,等到你用amount.toFixed()之类才在远处炸TypeError——定时炸弹。zod 在数据入口当场验,且能反推静态类型。
洞察 · 一条线收束全篇
如果三层里你卡住的题,几乎都能追溯到同一句话——「类型只在编译期、运行时被擦除;且按结构而非名字匹配」——那这份教程的目标就达成了。as 不安全、要用 zod、enum 特殊、泛型运行时为空、instanceof 不能用于 interface,全是这一条的推论,而不是五条要分别背的规则。