怪异的部分 — evolving any、开放对象与两个世界¶
这一章讲一件事: TypeScript 那些「咦,怎么这样」的行为,背后都是设计决定。 对象是开放的、类型和值是两个世界、函数可以少参数——三条主线, 串起 evolving any、多余属性检查、Object.keys 的固执、空对象类型的怪名、 跨界的 class 与 enum、this 的类型用法、函数联合的收成交集。 原书的期望写在结尾:「希望这一章显得标题起错了——你看完不觉得怪, 只觉得这些是设计上的必然。」1
1. 顶层全景:三个心智模型统摄全章¶
模型一:对象是开放的
→ 多余属性只在「新鲜对象」上报错(陈旧对象放行)
→ Object.keys 只能返回 string[]
→ {} 不是「空对象」,是「除 null/undefined 外的任何东西」
模型二:类型世界 vs 值世界
→ 大多数名字只住在一个世界;类型不能当值用,值不能当类型用
→ 例外:class、enum、this、同名 type+value 可以跨界
模型三:函数可赋值性
→ 实现方参数可以比声明少(忽略多余的参数是合法的)
→ 函数联合的参数收成交集 → 不兼容时退化成 never
图说:本章每个怪癖,都挂在其中一根钉子上。记住钉子,怪癖变成推论。
2. evolving any:类型会「进化」的变量¶
解决什么问题: JavaScript 变量经常「先空着、用的时候再变」。普通 TypeScript 变量做不到(类型是静态的),但有一个例外形态:
let myVar; // 悬停 → let myVar: any
myVar = 659457206512;
console.log(myVar.toExponential()); // ✅ 数字的方法
myVar = "mf doom";
console.log(myVar.toUpperCase()); // ✅ 字符串的方法
只写 let 不标类型,TypeScript 给的是 any——但不是普通的 any。
普通 any 上任何方法都放行(第 04 章);evolving any 上,
每次赋值之后,变量的类型就「进化」成新值的类型:
myVar = 659457206512;
myVar.toUpperCase(); // ❌ Property 'toUpperCase' does not exist on type 'number'.
赋值成数字之后,它就是 number——number 没有的方法,照样报错2。 原书称之为「一种高级形式的收窄」:类型跟着赋值动作,一步步收窄到当前值的类型3。
先做个对照实验,看清 evolving any 和普通 any 的分界:
let a: any = 123;
a.toUpperCase(); // ✅ 不报错——普通 any 对一切方法放行(第 04 章)
let b; // evolving any
b = 123;
b.toUpperCase(); // ❌ 报错:b 此刻是 number
区别在「是否被赋值驯化」:显式标注的 any 是「永久豁免权」, evolving any 是「临时的、随赋值更新的诚实记录」。原书那句「This doesn't work like a regular any」指的正是这条分界2。
数组版本更精彩,也是实战中最常用的形态:
const evolvingArray = []; // any[]
evolvingArray.push("abc"); // string[]
evolvingArray.push(123); // (string | number)[]
evolvingArray.push("do re mi"); // (string | number)[]
evolvingArray.push({easy: true}); // (string | number | {easy: boolean})[]
空数组起步,每次 push,数组的元素并集就扩一圈——类型从你的操作序列里被推出来4。
把数组进化多看一层,它的机制和第 06 章的「收窄」其实同构——都是「类型跟随证据更新」:
收窄(第 06 章):值不变,检查添加证据 → 类型在代码块内变窄
进化(本节): 检查(赋值/push)就是值本身的更新 → 类型跟着值的历史变
两者的共同前提:编译器只信任「已经发生的事」。
收窄信的是「if 里的检查发生了」;进化信的是「赋值/push 发生了」。
而它们的对称盲区也相同:未来的事一概不信——
进化后的变量重赋值成新类型,类型立刻再改写;想锁定,只有显式标注。
图说:收窄和进化是同一枚硬币的两面——收窄「验证后收紧」,进化「操作后重写」。
学会一个,另一个的边界也就清楚了。
这个同构关系解释了为什么 evolving any 的上手感觉「似曾相识」: 它就是把第 06 章的收窄逻辑应用到了「声明即历史」的场景。 也给出了它的使用边界:变量的一生里,如果类型改写的次数 ≤ 3, evolving any 是捷径;超过 3 次,显式并集标注(第 06 章)读起来更省—— 改写次数就是它的复杂度预算,和第 17 章 /utils 层的预算思维同构。
判断(我们的,不是书里的): evolving any 是「渐进类型」哲学的极端样本: 它本质上是在类型系统里为 JavaScript 的动态惯用法留的一条暗道。 实用建议:它是脚本和数据处理管道的好朋友(逐行解析、逐步聚合的场景极其顺手), 但在应用代码里要克制——它的类型是「历史操作的记录」,不是「契约」, 重构时没有任何地方能告诉你「这个变量应该是什么形状」。 一个团队准则:evolving any 出现的文件,值得多看一眼它是不是该写成显式类型。 如果错,会错在: 在快速原型和一次性数据处理里,显式类型的契约价值接近零, 而 evolving any 的开发速度快得多——「该写显式类型」的阈值随项目寿命上升, 本条建议对短命代码是过度要求。
evolving any 与第 08 章「拓宽」的正面对照¶
把本章的 evolving any 和第 08 章的「拓宽(widening)」并排,一个常被混淆的 点对点就清楚了——它们方向相反:
拓宽(第 08 章):声明时给了宽类型 → 之后赋值收窄,编译器跟着走
let x: string | number = "a"; x = 1; // 合法,x 的类型不变,值在槽内换
进化(本章):声明时没给类型 → 每次赋值,类型本身改写
let y; y = "a"; y = 1; // 合法,类型先 string 后 number
图说:拓宽是「槽已定,换值」;进化是「槽随值变」。
前者是静态类型的常规;后者是类型系统对动态惯用法的让步。
这个对照也解释了为什么「let 不标类型」是个双刃写法:它在脚本阶段给你
JavaScript 的自由,在应用阶段拿走 TypeScript 的保障。同一段代码,
换场景就是换评价——诊断「这里该不该加类型标注」时,先问这段代码的
预期寿命和读者数量,答案比任何规则都准(这条判据和第 17 章的复杂度预算同构:
预算 = 寿命 × 读者数)。
3. 多余属性警告:新鲜对象被盘查,陈旧对象放行¶
本章第一个「咦」。接口 Album 只要 title 和 releaseYear:
const rubberSoul = {
title: "Rubber Soul",
releaseYear: 1965,
label: "Parlophone", // 接口里没有的「多余属性」
};
processAlbum(rubberSoul); // ✅ 不报错!
先存变量再传——多余属性安然无恙5。把两次悬停放在一起看,命运的岔路口很清楚:
路径 A(先存变量):
const rubberSoul = { title, releaseYear, label };
processAlbum(rubberSoul); // ✅
悬停 rubberSoul → { title: string; releaseYear: number; label: string }
——推断保留了 label;传给 processAlbum 时,比较发生在
「一个已有类型」和 Album 之间 → 陈旧对象 → 放行
路径 B(内联字面量):
processAlbum({ title, releaseYear, label });
——比较发生在「这个字面量」和 Album 之间 → 新鲜对象 → 报错
报错:Object literal may only specify known properties,
and 'label' does not exist in type 'Album'.
两条路径的区别只在比较发生在哪一侧,而不在对象的内容—— 这就是「检查跟着字面量走」的精确含义56。
但如果内联:
processAlbum({
title: "Rubber Soul",
releaseYear: 1965,
label: "Parlophone", // ❌ Object literal may only specify known properties
});
同一个对象,两种命运。区别的正式名称是新鲜度(freshness):
- 新鲜对象(fresh object):在赋值现场直接写出来的字面量——声明变量时, 或内联进函数调用时。新鲜对象接受多余属性检查:字面量里的每个键都必须在 目标类型里存在,否则报错6。
- 陈旧对象(stale object):先赋给变量、或从函数返回的对象。陈旧对象 不做多余属性检查,按开放对象对待7。
设计理由(原书给了两段):陈旧对象可能在别处还要用到那个多余属性, TypeScript 不想挡你的路;而内联对象「肯定不会在别处用到」,查一查是净收益8。 所以推论很反常识:TypeScript 并不是「在乎」多余属性——它只在乎「你现在写的这个字面量」里的多余属性。
这个行为的代价有一个经典受害者场景:可选参数名拼错。
const fetch = (options: {url: string; timeout?: number}) => { /* ... */ };
const options = {
url: "/",
timeOut: 1000, // 拼错了!timeout 少了个字母,变成多余属性
};
fetch(options); // ✅ 不报错 —— 陈旧对象放行
// 运行时:timeout 永远是 undefined,行为静默错误
唯一的办法是给 options 变量加上类型注解——注解让对象在赋值现场变「新鲜」, 拼错的键立刻现形9。「可选参数不生效、且无报错」排查第一步:查拼写。
「拼写漏网」的完整事故树与三档防线¶
这个 misspelled 案例是全书最典型的「类型系统安静地帮倒忙」场景,值得把事故树 和防线画全:
事故树:
① 写代码:options 对象手抄了参数名(timeOut vs timeout)
② 传参:fetch(options) —— 陈旧对象放行,编译器沉默
③ 运行时:timeout 永远是 undefined,功能静默失效
④ 排查:无报错可追,只能从「功能不生效」倒推
三档防线(按成本从低到高):
第一档:调用处内联对象(零成本)
fetch({ url: "/", timeOut: 1000 }); → 拼错立刻报错(新鲜字面量)
适用:一次性调用。
第二档:变量标注(一次成本)
const options: FetchOptions = { url: "/", timeOut: 1000 }; → 声明处报错
适用:options 会被复用。
第三档:satisfies(第 12 章工具,两全)
const options = {...} satisfies FetchOptions; → 检查 + 推断保留
适用:既想复用对象,又需要它原本的精确推断(如 keyof 推导)。
图说:三档防线对应对象「用几次、要不要精确推断」两个维度的组合;
漏网的共同原因是「先存变量、忘了标注」——事故树的 ② 正是这一步。
三档防线的选择逻辑,和第 12 章的「沟通协议卡」完全同构(检查失败 → 找依据 → 选工具)。misspelled 这类事故的特征是「静默」——它不产生报错,只产生行为 偏差,所以等不到编译器救你,只能靠防线前置。把「options 对象要不要标注」 从个人习惯升级成团队代码风格(lint 规则可强制),是这类事故唯一可靠的终结方式。
函数比较时也不查多余属性¶
第二种不查的场景:把函数传给函数时,返回对象的多余属性不查:
const newAlbums = remapAlbums(albums, (album) => ({
...album,
releaseYear: album.releaseYear + 1,
strangeProperty: "This is strange", // ✅ 不报错!
}));
没有返回类型注解时,TypeScript 在比较两个函数,而函数之间的比较
不触发多余属性检查——于是每个 Album 运行时都多一个 strangeProperty,
编译器还以为返回的是干净的 Album[]10。修复:给内联回调加返回类型注解
((album): Album => ...),返回的字面量变「新鲜」,检查恢复11。
开放 vs 封闭:与 Flow 的对照¶
TypeScript 的对象默认开放(open)——任何对象都可能有多余属性。
Meta 的 Flow 走了另一条路:对象默认封闭(exact),多一个属性直接报错,
要用 ... 语法显式声明「允许开放」12。
原书明说了立场:TypeScript 的选择更合理——开放对象更贴近 JavaScript 的真实 运行方式;在这么动态的语言上,任何类型系统自称「绝对安全」都值得警惕13。 但团队也做了折中(下一节的 fresh/stale),原书称之为两全:比 Flow 的封闭宽松, 又在你「亲手写字面量」的瞬间给出你想要的警告14。
「开放」的世界观代价与收益清单¶
开放对象这个决定是本章全部怪癖的总根源,值得把它的代价和收益摆全:
收益(为什么该开放):
① JSON 反序列化多出来的字段、框架注入的属性、扩展对象——全部无缝通过
② 库升级加字段,消费方代码不报错(封闭对象会全线爆炸)
③ 与 JavaScript 的鸭子类型心智完全对齐——学习成本零
代价(为什么它制造怪癖):
① 多余属性检查只能放在「字面量现场」(fresh/stale 二分)
② Object.keys 只能返回 string[](键集不可枚举,第 4 节)
③ {} 变成「非空」而非「空对象」(第 5 节)
④ 拼错的可选参数名在陈旧对象上漏网(第 3 节的开头)
图说:四条代价,三条是「键集不可知」的推论,一条是「检查时机」的限制——
同一个决定(开放)在不同地形上的投影。认清总根源的好处:
遇到一个没见过的怪癖,先问「它是不是开放对象的投影」,八成是。
这个「总根源视角」也是本章的阅读策略:不必把六个怪癖当六个孤立事实背, 记住「一切皆开放」一个决定,六个怪癖全是它的推论——这是本章从「怪癖集」 升级成「世界观」的关键一跃,也是原书把 Flow 对照放在这里的深意: 两个世界观的分岔,解释了后面所有行为差异的走向。
练习 12-2 走查:同一个对象的三种「变新鲜」方式¶
练习 12-2 把 fresh/stale 玩成了一道应用题:options 对象带着多余属性
search,上面的 @ts-expect-error 因为不报错而自己亮了红线(预期落空)。
任务是「让多余属性检查重新生效」,标准答案有三个,恰好对应三种「把对象变新鲜」的姿势:
// 姿势一:变量注解——赋值现场变新鲜
const options: FetchOptions = { url: "/", /* ..., */ search: new URLSearchParams() };
// ↑ 多余属性检查恢复,@ts-expect-error 兑现
// 姿势二:satisfies——检查不动推断
const options = { url: "/", /* ..., */ search: new URLSearchParams() } satisfies FetchOptions;
// 姿势三:内联到调用处——字面量直接出现在参数位置
myFetch({ url: "/", /* ..., */ search: new URLSearchParams() });
三种姿势的检查原理完全相同(字面量 vs 类型的直接比较),选用看场景:
姿势一最直白;姿势二要接着用 typeof 推导键的类型;姿势三适合一次性调用51。
这道题的真正考点是:@ts-expect-error 是个测试断言,断言失败要先问「被测行为
为什么没发生」——在这里就是「对象为什么不新鲜」。
练习 12-3 走查:map 回调的盲区¶
const usersWithIds: User[] = users.map((user, index) => ({
...user,
id: index,
age: 30, // @ts-expect-error 期望报错,但没报
}));
age 不是 User 的属性,为什么检查不生效?把 §3 的第三种不查场景套上来:
map 回调的返回值没有类型注解时,TypeScript 在比较的是「推断出的返回类型」和
User[]——两个类型(函数产出的)之间的比较,不是字面量和类型的比较。
返回值此刻是「陈旧」的,多余属性放行50。
两个修复,各对应一种「让返回值变新鲜」:
// 修复一:给回调标返回类型
users.map((user, index): User => ({ ...user, id: index, age: 30 }));
// ↑ 现在报错,@ts-expect-error 兑现
// 修复二:satisfies 落在返回的字面量上
users.map((user, index) => ({ ...user, id: index, age: 30 }) satisfies User);
原书对这类盲区的防御守则是:「可能含多余属性的变量,总是显式声明类型; 函数,总是显式声明预期返回类型」50。这句守则是全章性价比最高的一条工程纪律。
判断(我们的,不是书里的): fresh/stale 机制是「实用主义类型设计」的教科书案例: 它把「严格性」精准地投放到了「开发者最可能犯错的位置」(亲手写的字面量), 而把「灵活性」保留给了「系统的接缝处」(变量传递、库边界)。 代价是机制本身必须被学习——本章的存在就是这个代价。Flow 的封闭默认在理论上更干净, 但在「和 JavaScript 生态共存」这个实际考题上输了。 如果错,会错在: 随着 TypeScript 控制流分析增强,未来可能对陈旧对象也做 有限的多余属性检查(社区确实有过提案);若成真,「陈旧对象放行」将变成 「仅在跨模块边界放行」,本章的机制描述要收窄——但 fresh/stale 的二分本身 大概率保留,因为它服务的是「字面量检查」这个真实需求。
4. 对象键是松散类型:Object.keys 的固执¶
开放对象的直接后果:编译器不能假设对象的键就那么多——万一有多余属性呢?
const yetiSeason = { title: "Yeti Season", artist: "El Michels Affair", releaseYear: 2021 };
const keys = Object.keys(yetiSeason); // 类型:string[]
Object.keys 的返回类型是 string[],不是 (keyof typeof yetiSeason)[]。
于是用这些键去索引对象,撞墙:
keys.forEach((key) => {
console.log(yetiSeason[key]);
// ❌ expression of type 'string' can't be used to index type '{title...}'
});
string 太宽了——它可能不是这个对象的键。唯一合法的键类型是
'title' | 'artist' | 'releaseYear'(keyof typeof),而 keys 不是它15。
for...in 循环同样的问题16。
把「string 太宽」翻译成类型语言,这个报错其实是宽窄规则(第 06 章)的又一次值班:
索引访问 obj[key] 的类型规则:key 的集合必须「窄进」obj 的键集合。
keys: string[] → 键集合是「任意字符串」(宽)
yetiSeason 的键集合 → "title" | "artist" | "releaseYear"(窄)
宽进窄 → 拒绝(和第 06 章「宽进窄不行」一字不差)
Object.keys 的设计选择:返回宽类型 string[] 而非窄类型 (keyof T)[],
是 TypeScript 团队对「保护优先」的又一次表态——
宁可让合法的窄键访问报错(逼你显式收窄),也不让宽键访问在运行时悄悄落空。
这个选择和开放对象世界观一脉相承:键集不可枚举,宽键类型是唯一诚实的回答。
图说:报错不是刁难,是宽窄规则在键位置的应用;
「为什么不让 Object.keys 返回 keyof T[]」的答案:那会谎报键的安全性,
一旦对象形状漂移(第 3 节的陈旧对象放行),遍历就会漏字段——string[] 至少诚实。
「诚实胜过方便」是这个设计的三连注脚:开放对象诚实(至少包含)、Object.keys 诚实 (键集宽)、函数参数诚实(少收合法)——三个设计共享同一个价值观, 这就是原书所谓「可预期的推论」的真正含义:不是随机的妥协,而是同一条原则 在不同地形的贯彻。读者一旦认出这条贯穿线,剩下的「怪癖」就只是这条线的 新段落,而不是新的惊吓。
这条贯穿线也值得和第 10 章的「JavaScript with types」对照着看: 第 10 章讲的「薄语法」是这条线的正面表述(类型只加注解、不改语义), 本章讲的三个「放弃」是它的反面表述(为了保证正面成立,类型系统必须 放弃一部分理想化的能力)。正面与反面合起来,才是 TypeScript 设计哲学的完整 形状——「贴着 JavaScript 走」既是它的力量来源(零学习成本、生态兼容), 也是它的妥协来源(开放对象、string[] 键、少参数合法)。两个来源是同一枚硬币, 拆开任何一面讲都会讲偏——这是本章与第 10 章合读时才完整的一次论证, 也是原书把两章分得这么远(第 10 章与第 13 章)却暗中呼应的又一例。
原书强调:这份固执是保护,不是刁难。 看一个反例——用 as 强绕过去之后:
const staleAlbum: Album = yetiSeason; // 陈旧的,带 releaseYear
for (const key in staleAlbum) {
// 编译时不报错;运行时炸:
// "staleAlbum[key].toLocaleLowerCase is not a function"
console.log(staleAlbum[key as keyof Album].toLocaleLowerCase());
}
keyof Album 是 "title" | "artist",可遍历出来的键实际还包括 releaseYear——
强绕的检查缺口在运行时变成崩溃。枚举键时最安全的默认就是把键都当 string 处理17。
练习 12-4 给了四种遍历的正道,按推荐排序:
- Object.values(如果只需要值):绕开键问题 entirely18;
- 参数放宽:
printUser(obj: Record<string, unknown>)——函数本来就只想 「遍历任意对象的键值」,把参数类型说真话,索引访问自然合法19; - as keyof:断言键是
keyof typeof obj——能用,但接上例的风险, 对象声明类型窄于实际形状时会漏20; - for...in 版同理(
key as keyof typeof user)21。
补充一个第 ⑤ 种选项,2026 年的日常首选:Object.entries
Object.entries(user).forEach(([key, value]) => { ... });
一次拿到键和值,键仍是 string(同样的诚实),值是 unknown 级别的宽——
比 Object.values 多给了键,比 as keyof 安全。配合 Record<string, unknown>
参数(选项 2)使用,是「键值都要」场景的最优解。
(补充,来自通用知识:Object.entries 的返回类型是 [string, V][],V 来自参数类型。)
四种正道加一个补充选项,覆盖了遍历的全部需求形状:
| 需求 | 首选 | 次选 |
|---|---|---|
| 只要值 | Object.values | —— |
| 键值都要 | Object.entries(配宽参数) | as keyof(有前提) |
| 函数本职是遍历任意形状 | 放宽参数(选项 2) | Object.entries |
| 必须保住原契约且形状可信 | as keyof(选项 3/4) | for...in 版 |
把「需求 → 首选」的映射记住,遍历问题从此是查表题而不是推理题—— 这也是本章教法的缩影:每个怪癖背后,都对应一张「需求 → 正解」的映射表; 表背熟了,怪癖就从「惊吓」变成了「索引」。
把四个选项走完整,各自的安全边界就清楚了:
// 选项 1:Object.values —— 不碰键,最安全(只要值不要键时)
function printUser(user: User) {
Object.values(user).forEach(console.log);
}
// 选项 2:放宽参数类型 —— 函数说真话:「我遍历任意形状」
function printUser(obj: Record<string, unknown>) {
Object.keys(obj).forEach((key) => {
console.log(obj[key]); // ✅ string 索引 Record 完全合法
});
}
// 选项 3:as keyof —— 键断言,快但接上文的风险
function printUser(user: User) {
Object.keys(user).forEach((key) => {
console.log(user[key as keyof User]); // ✅ 但前提是 user 的形状 == 实际形状
});
}
// 选项 4:for...in 版同选项 3
function printUser(user: User) {
for (const key in user) {
console.log(user[key as keyof typeof user]);
}
}
选项 1 在「只要值」时是显然最优;选项 2 改变了函数的契约 (从「收 User」变成「收任意对象」),调用方泛了,但函数体也更诚实了; 选项 3、4 保住了契约,代价是键访问的 soundness 缺口(第 4 节开头的崩溃反例)。 练习的加分项「试试拓宽参数类型」,其实就是在推你发现选项 2—— 很多时候「遍历问题」的正确解法不是更聪明的键处理,而是把参数类型放宽成 函数真正需要的样子19。
两条路的取舍(原书原话):「要么让键访问稍微不安全(as keyof typeof), 要么放宽被索引的类型——两条都能用,看你的场景。」22
遍历问题的「决策树」版本¶
把四个选项收成一个决策树,实战时按顺序问三个问题:
问 ①:我只需要值,不需要键吗?
是 → Object.values(选项 1),结束。零风险,首选。
问 ②:这个函数的本职是「操作 User」还是「遍历任意形状」?
后者 → 放宽参数(选项 2):Record<string, unknown> 或 object,
让签名说真话。结束。
前者 → 进入问 ③。
问 ③:对象的「声明形状」和「运行时形状」能保持一致吗?
能(类型是单一事实源,无外部注入) → as keyof(选项 3/4)可用。
不能(接口来自边界、可能有多余字段) → 回到选项 1 或 2,
或者承认「这个函数需要运行时校验」——类型层面的遍历到此为止。
图说:三个问题把四个选项整理成「默认安全、逐步放宽」的顺序;
as keyof 从「第一手段」降级成了「最后一道」,这是全章最重要的排序修正。
这个排序修正的价值在真实项目里立竿见影:新手的本能是选项 3(as keyof 一行解决), 而它也是四个选项里唯一制造过运行时崩溃的(第 4 节开头的 toLocaleLowerCase 反例)。 「一行解决」和「正确解决」在类型世界里经常分叉——决策树的意义, 就是把两者在「问 ②」处重新合流:签名说真话的选项 2,往往比强绕的选项 3 既安全又少打字。
决策树背后的「签名说真话」原则¶
把决策树的三个问题再抽象一层,它们其实是同一个原则的三次应用: 让函数签名说真话。
「签名说真话」的含义:参数类型 = 函数实际需要的最宽形状,不多承诺也不少承诺。
承诺过宽(收 User 却只遍历)→ 强绕键检查(as keyof)来弥补谎言;
承诺过窄(收 unknown 却当 User 用)→ 收窄链来还愿;
承诺刚好(收 Record<string, unknown>)→ 函数体零谎言,调用方也零惊讶。
图说:签名是说给两个听众的合同——编译器(检查用)和读代码的人(理解用)。
两个听众都信合同,合同就得是真的。as keyof 的本质是「合同是假的,
但我用断言把执行掰回合同」——短期可行,长期债务。
这个原则也是第 12 章「运行时依据」判据的正向版:第 12 章说「断言要有依据」, 这里说「签名要有依据」——一个管「改类型」,一个管「定类型」,共享同一个 「诚实」内核。把「签名说真话」写进团队的 code review 清单, 遍历问题、断言泛滥、谓词撒谎这三类事故会同时降温——它们都是签名撒谎的 不同症状。
5. 空对象类型 {}:「除 null 和 undefined 之外的一切」¶
开放对象模型的推论之二,也是本章最反直觉的一个:
const coverArtist: {} = "Guy-Manuel De Homem-Christo"; // ✅ 字符串可以!
const upcCode: {} = 724384260910; // ✅ 数字可以!
const submit = (homework: {}) => console.log(homework);
submit("Oh Yeah"); // ✅
submit(null); // ❌ Argument of type 'undefined' is not assignable to '{}'
{} 不代表「空对象」,它代表「除 null 和 undefined 之外的任何东西」——
string、number、boolean、函数、symbol、带属性的对象,全部放行23。
理由正好接上开放对象:字符串和数字在 JavaScript 里也有属性(方法),
它们是「最开放的对象」;而 null/undefined 是唯一访问属性必崩的东西——
它们才真的不符合「对象」的定义24。原书给的评价很高:「把 {} 定义成
『非 null 非 undefined』,是这个问题的一个相当优雅的解。」25
练习 12-1 就是这个类型的一次实战应用:写一个「什么都能收、就是不能收
null/undefined」的函数,答案就是一个 : {} 参数26:
const acceptAnythingExceptNullOrUndefined = (input: {}) => {};
acceptAnythingExceptNullOrUndefined("hello"); // ✅
acceptAnythingExceptNullOrUndefined(42); // ✅
acceptAnythingExceptNullOrUndefined(true); // ✅
acceptAnythingExceptNullOrUndefined(Symbol("foo")); // ✅
acceptAnythingExceptNullOrUndefined({}); // ✅
acceptAnythingExceptNullOrUndefined([]); // ✅
acceptAnythingExceptNullOrUndefined(() => {}); // ✅
acceptAnythingExceptNullOrUndefined(/foo/); // ✅ 正则也行
acceptAnythingExceptNullOrUndefined(new Error()); // ✅
acceptAnythingExceptNullOrUndefined(null); // @ts-expect-error ✅ 被拦
acceptAnythingExceptNullOrUndefined(undefined); // @ts-expect-error ✅ 被拦
十一种值全过、唯二空值被拦——{} 的语义一眼看清。
(注意别和「空对象字面量的类型」混淆——const x = {} 推断出的是 {},
但那是「暂无已知属性的对象」,可赋值性规则同上。)
{} 与 unknown 的分工:都是「全收」,区别在出口¶
{} 和 unknown 都「什么都能进」,日常选型容易混,分界线在出口:
unknown:进口全收,出口全锁——想用必须先收窄(第 06 章的梯子顶)
{}: 进口全收(除 null/undefined),出口松一半——
可以直接访问「任何非空值都有的能力」(toString、valueOf),
但访问具体属性仍需进一步收窄
图说:unknown 是「完全未知的行李」,开箱前不许动;{} 是「知道不是空的行李」,
至少可以把它当「东西」对待(打印、转字符串),但里面有什么仍不知道。
实用判据:参数的类型源头决定选谁。
源头是「网络/JSON/表单」(可能连 null 都有) → unknown,收窄链走完再干活;
源头是「框架保证非空的上下文对象」→ {} 或更精确的对象类型,直接可用。
把 {} 当 any 的替代品(「反正什么都能进」)是本章最常见的误用——它放行的
只是进口,出口的锁比 any 紧得多;误用的症状是 {} 参数体内一堆红线,
那正是出口锁在值班。
6. 主线小结:一个决定的三波推论¶
开放对象这一个决定,在本章连放三枪:多余属性检查只在新鲜对象上(第 3 节)、 Object.keys 只能给 string、{} 变成「非空」的别名(第 5 节)。 学到这里,第一个心智模型已经立住——「TypeScript 的对象类型是『至少包含这些属性』, 而不是『恰好只有这些属性』」。剩下两节(两个世界、函数可赋值性)是另外两根钉子。
判断(我们的,不是书里的): 「至少包含」这个语义,是 TypeScript 能覆盖 JavaScript 生态的关键设计:JSON 反序列化多出来的字段、框架注入的属性、 扩展对象——全都靠它无缝通过。它牺牲的是「形状精确」的幻觉: 你以为你的 User 恰好三个字段,运行时它可能有五个。 精确性要用别的方式买回来(第 06 章的收窄、运行时校验),而不是指望对象类型。 如果错,会错在: 对于强契约场景(API 边界、序列化协议),「至少包含」太松, 此时 schema 校验(第 06 章的 Zod 路线)才是契约的本体,类型只是它的投影—— 这个限定条件原书没有明说,我们补上。
「至少包含」的精确化:与「精确形状」的三种握手方式¶
「至少包含」听着像缺陷,但工程里有三种正规方式把它精确到契约级:
方式一:fresh 检查(本章内置)——只在字面量现场生效,覆盖面窄但零成本
方式二:判别联合(第 06 章)——用判别子把「哪些字段存在」编码进类型,
配合 switch 的穷尽检查,达到「每个状态的形状恰好」的效果
方式三:运行时 schema 校验(第 06 章点名的 Zod 路线)——
值在进入应用时被验证,验证器的类型推导让「精确形状」进入类型世界
图说:方式一管声明现场,方式二管结构互斥,方式三管运行时边界——
三者各管一段,合起来覆盖了「精确形状」的全部需求场景。
没有覆盖不到的场景,只有「该用哪种而没用」的错配。
这个三方式清单也是本章与第 06 章的分工说明:第 06 章教你用判别联合表达 「结构的互斥」,本章教你「对象默认是开放的、精确要用工具买」—— 两章合观,才是「对象形状精确性」的完整管理方案。跳过本章读第 06 章, 会误以为判别联合是「唯一正解」;跳过第 06 章读本章,会误以为精确性无解。 教材的章节顺序在这里又一次显出用心:先给解法(第 06 章),再揭限制(本章), 读者在知道「能做什么」之后,才被告知「默认是什么」——顺序反过来, 前者的动力会被后者的悲观稀释。
「至少包含」的边界测试:三个快问快答¶
把「至少包含」的语义用三个快问快答钉死,以后遇到具体代码可以直接对号:
问 ①:{ a: 1 } 能赋给 { a: number; b: string } 吗?
不能。后者要求的 b,前者没有——「至少包含」意味着承诺的每个属性都必须兑现。
宽承诺 → 窄满足,和「宽进窄不行」同向。
问 ②:{ a: 1, b: "x", c: true } 能赋给 { a: number } 吗?
能(陈旧对象)。c 是多余属性,开放世界观下照单全收。
但注意 fresh 二分:这是「先存变量再传」,内联字面量则会被查。
问 ③:让「恰好只有 a」成为类型,怎么写?
判别联合:{ kind: "x", a: number } 单成员联合——
配合 switch 的穷尽检查,等于把「恰好」钉进了类型。
或者用 schema 校验(三方式之三)在运行时把关。
图说:三问答完,「至少包含」的边界、例外、强化手段各归其位——
这条语义从此不需要再「想想」,只需要「对号」。
这组问答也是本章学习效果的自检:能不看答案答对三问,第 6 节的世界观就过关了; 答错任何一问,回到对应小节重读——问答指向的正是最容易被「差不多懂了」 盖过去的三个缝隙。
7. 类型世界与值世界:一条严格、可跨的边界¶
第二根钉子。TypeScript 的语法空间分成两个世界,同一行代码里可以并存:
myNumber 和 42 是值;number 是类型。关键字还被两个世界复用:
if (typeof key === "string" && (key as keyof typeof obj)) {
// └──── 运行时:值 ────┘ └───── 类型世界 ─────┘
}
同一个 typeof,在值的位置是运行时运算符,在类型的位置是类型提取——
编译器按位置分辨语义27。
边界规则很硬,两个方向都试不通:
type Album = { title: string; artist: string; };
processAlbum(Album); // ❌ Cannot find name 'Album' — 类型在值世界不存在
type T = ReturnType<processAlbum>; // ❌ 'processAlbum' refers to a value,
// but is being used as a type here.
// Did you mean 'typeof processAlbum'?
类型不能当值用(擦除了),值不能当类型用(没提取)——报错信息还顺手教你怎么修
(typeof processAlbum)28。
跨界者一号:class¶
第 09 章见过:类同时活在两个世界。Song 作为类型指实例的形状;
作为值它是构造器函数本身:
const playSong = (song: Song) => { /* ... */ };
playSong(new Song("Song 1", "Artist 1")); // ✅ 传实例
playSong(Song); // ❌ Argument of type 'typeof Song' is not assignable to 'Song'
报错里的 typeof Song 就是「构造器函数这个值」的类型——
类名当类型用 = 实例;类名当值用 = 构造器29。
跨界者二号:enum¶
枚举同样两界居住。AlbumStatus 作为类型指它的成员;
作为值是那个真实存在的对象:
function logAlbumStatus(status: AlbumStatus) { /* 收成员 */ }
// 想要「整个枚举对象」?typeof 取:
function logWholeEnum(status: typeof AlbumStatus) { /* 收 {NewRelease: 0, ...} */ }
原书的提醒:当类型用时,enum 指成员不是整体——想表达「整体」要 typeof30。
跨界者三号:this¶
类里 this 也跨界:this.playCount(值,访问属性)和 play(): this
(类型,指「当前类的实例」)出现在同一个方法里31。
this 作为返回类型的回报是链式调用:
class Song {
play(): this {
this.playCount += 1;
return this;
}
}
const earworm = new Song("Mambo No. 5").play().play().play(); // 链!
原书点出一个少见的等价:play(): this 和 play(): typeof this 是一回事
——都指向当前实例32。
this 返回类型的工程价值:多态 this(fluent 接口)¶
play(): this 这个小小的返回类型声明,是流式接口(fluent interface)
的类型基础:
链式调用的类型难题:父类方法返回「父类类型」,子类实例一链式,类型就丢回父类
class Query { where(): Query { return this; } }
class AlbumQuery extends Query { sort(): AlbumQuery { ... } }
new AlbumQuery().where() ← 类型退化成 Query,sort 不见了!
返回 this 的解法:where(): this —— 返回类型跟着「实际调用者」走
new AlbumQuery().where() ← 类型仍是 AlbumQuery,sort 照常补全
图说:this 作为返回类型 = 「返回类型 = 调用者的精确类型」,
编译器把多态性从「运行时动态分发」挪进了「类型层面的保留」。
这个模式在 2026 年的库代码里随处可见(查询构建器、测试框架的 expect 链、
Immutable 风格的集合操作)。它也是「类型即契约」的一次精采演示:
一个词的返回类型声明(this),换来子类链式调用的全程类型保留——
成本接近零,收益贯穿整个继承体系。原书只点了这个等价关系,它的工程应用
(fluent interface)是我们补的(补充,来自通用知识);但等价关系本身
(this ≡ typeof this)是原书明写的,是理解后续应用的地基。
跨界者四号:同名类型 + 同名值¶
最后一种跨界是「故意重名」。一个值和一个类型同名,各住各的世界, 使用者随时切换:
export const Track = {
play: (title: string) => { /* ... */ },
pause: () => { /* ... */ },
stop: () => { /* ... */ },
};
export type Track = typeof Track; // 类型版:值的形状的镜像
使用处:track: Track 参数(类型世界),Track 实参(值世界)——
悬停 import 会显示两条别名:一条 type、一条 const33。
这个模式的杀手锏应用,是给第 10/11 章的 as const 枚举再加一层:
export const Direction = {
Up: "up",
Down: "down",
Left: "left",
Right: "right",
} as const;
export type Direction = (typeof Direction)[keyof typeof Direction];
一个名字拿到手:值(Direction.Up)、类型("up" | "down" | "left" | "right")、 两者同步(类型从值推导)34。这就是 2026 年 TypeScript 代码库里 「枚举」事实上的标准形态——前面几章的伏笔在这里合拢。
同名双声明的设计用途:不止枚举¶
Track/Direction 模式的价值不止于「替代 enum」,它在三种设计场景里是正解:
场景一:常量 + 类型的同步需求(本章主例)。
值用于运行(遍历、查找、默认值),类型用于标注(参数、返回值)——
同名双声明让两者共用一份定义,改常量自动改类型(第 11 章的单一事实源)。
场景二:插件/扩展点注册表。
const WidgetRegistry = { button: ButtonWidget, chart: ChartWidget } as const;
type WidgetType = typeof WidgetRegistry; // 注册表本身也是类型
type WidgetKind = keyof WidgetRegistry; // 键联合
加一个新 widget = 注册表加一行,类型世界的两个联合自动跟上。
场景三:与库的类型对接。
库导出的是值,你的代码需要的是类型——typeof 取它的类型,
加 satisfies 校验你自己的扩展版本(第 12 章的组合技)。
图说:三个场景的共同点——「运行时有一份真值,类型世界需要它的投影」。
同名双声明是这个投影的最短路径:一次定义,两个世界。
第三种场景尤其值得记住,因为它解释了为什么这个模式在 2026 年的库代码里 无处不在:schema 优先的库设计下,库导出常量/函数(值),用户用 typeof 取类型——同名双声明把「用库」和「扩展库」的接线缩短到一行。它也是第 14 章 声明文件的镜像:声明文件是「给 JavaScript 值补类型」,同名双声明是 「给 TypeScript 值同时声明类型」——方向相反,目标相同:值与类型的合一。
两个世界的「边界测试」:四个快问快答¶
和两个世界的分界线打交道,值得用四个快问快答把边界钉死:
问 ①:interface Album { ... } 之后,console.log(Album) 会怎样?
❌ Cannot find name 'Album' —— 类型世界里没有 Album 这个值。
问 ②:const album = {...} 之后,type T = album 会怎样?
❌ 'album' refers to a value... Did you mean 'typeof album'? —— 值不能当类型。
问 ③:class Song { ... } 之后,new Song() 的类型是?
Song —— 类是跨界者,值(Song 构造器)和类型(Song 实例)同名共存。
问 ④:type X = typeof someValue 之后,X 是值还是类型?
类型 —— typeof 是值世界 → 类型世界的转换器,产物是类型。
图说:四问答完,两个世界的「门」和「跨界者」各归其位。
答错的每一问,都对应一个具体的报错文本——报错文本本身就是边界地图,
把它背下来,比记规则快。
这组问答和第 6 节的三问答是同族工具:用快问快答把「差不多懂了」的缝隙显影。 两组合起来,本章的两个心智模型(开放对象、两个世界)就都有了可自检的闭环—— 这也是「模型级」知识和「规则级」知识的分水岭:规则靠记,模型靠测; 能出题自测的,才是模型。
这个「模型靠测」的分水岭,也值得和第 02 章的「悬停驱动」并起来看: 悬停是模型的输入端(看见编译器眼中的世界),自测问答是模型的输出端 (验证你脑中的世界是否与之相符)——两端都通了,模型才算闭合。 只悬停不自测,容易把「看懂了」误当「掌握了」;只自测不悬停,容易把 「记熟了」误当「理解了」。本章的两组问答(第 6 节三问、本节四问), 就是输出端的标准化考卷——建议读者合上书先做一遍,再对答案; 做错的题比看懂的段落值钱十倍,因为错题指出的正是「差不多懂了」的缝隙。
两个世界的实用推论¶
这套世界观解释了日常报错的一半。见到 'X' refers to a value, but is being
used as a type here,不必慌——报错信息里就写着药方(Did you mean 'typeof X'?);
见到 Cannot find name 'X',先想「X 是不是一个纯类型(接口/类型别名)」。
反过来,想「让一份数据同时作为值和类型被消费」时,本章给了两条正规路:
同名双声明(Track 模式)和 as const + 类型推导(Direction 模式)2834。
原书对这套机制的评价是「双份职责的功能可能非常有用,尤其当你想把
『感觉像类型』的东西在代码里多处复用时」33。
8. this 的类型用法:函数签名里的隐藏参数¶
this 在普通函数里也可以被标注——作为签名里的第一个「参数」:
function sellAlbum(this: {title: string; sales: number}) {
this.sales++;
console.log(`${this.title} has sold ${this.sales} copies.`);
}
注意两件事:调用时不用真的传 this(它指调用处的对象)35; 而且检查的时机反直觉——不在函数定义处查,在调用处查:
const album = { sellAlbum };
album.sellAlbum(); // ❌ this 上下文缺 title 和 sales
const album = { title: "Solid Air", sales: 40000, sellAlbum };
album.sellAlbum(); // ✅
报错的意思是:album 这个对象的形状,配不上 sellAlbum 声明的 this 形状36。
这个「调用处检查」的机制有个实用推论:this 参数是一种廉价的调用约束——
你在函数签名里写下 this: {title: string; sales: number},
等于声明「这个函数只能挂在有这些属性的对象上」,任何不满足的挂载点当场报错。
库代码里常见这种用法(jQuery 风格的插件、事件处理器),
它让「函数对宿主对象的要求」第一次变得可读可查。
补全属性后,this 在函数体内就有了完整类型。
箭头函数没有这个通道:(this: {...}) => {} 直接报错
(An arrow function cannot have a 'this' parameter)——因为箭头函数的 this
不从调用处继承,从定义处的词法作用域继承37。这条与第 09 章「箭头方法
this 绑实例」是同一个 JavaScript 语义的两面。
this 参数与「宿主契约」的设计模式¶
this 参数的最大价值不在工具函数,而在声明宿主契约:
场景:一组函数,设计上必须配合某个对象使用(事件处理器、插件钩子、
面向对象的回调)。不用 this 参数的写法,宿主要求散落在文档和注释里;
用了 this 参数,要求进了签名:
function onAlbumClick(this: AlbumElement, e: Event) { ... }
挂载点不满足 AlbumElement 形状 → 定义处...不,调用处当场报错。
收益清单:
① 要求可读:签名即文档,悬停可见
② 挂载点可检查:addEventListener 式注册处自动校验
③ 函数体内 this 有完整类型:补全、检查全部激活
代价清单:
① 只有 function 函数可用(箭头函数无此通道)
② 调用处检查的报错信息对新手绕(「'this' context of type X is not assignable...」)
③ 与面向对象的 class 方法重叠——class 体系内该用方法而不是 this 参数
图说:它是「结构化的宿主要求」——适合库边界和插件点,不适合类体系内部。
第 ③ 条代价值得展开:在 class 里,方法靠隐式 this(第 09 章), 再显式标 this 参数是冗余;但在「函数当一等公民传来传去」的场景 (事件注册表、插件数组),this 参数是给散落的函数补「宿主合同」的唯一通道。 选型口诀:函数被「挂」到对象上时用 this 参数,函数被「调」时用普通参数—— 挂是长期关系(要合同),调是一次性往来(普通参数就够)。
this 参数的报错阅读指南¶
调用处检查的报错文本对新手不友好,值得单独一份阅读指南:
典型报错:
The 'this' context of type '{ sellAlbum: ... }' is not assignable to
method's 'this' of type '{ title: string; sales: number; }'.
Property 'title' is missing in type '{ sellAlbum: ... }' ...
三行各管一段:
行 1(什么不匹配):this 上下文(= 调用处的对象)≠ 方法要的 this
行 2(缺什么):对象缺 title 和 sales
行 3(结论):所以配不上
阅读动作:先找「Property 'x' is missing」——它告诉你往宿主对象补什么;
补完即过。和并集报错「从底往上读」是同一个阅读纪律:
结论行在后,明细行在中,摘要行在前。
图说:this 参数的报错,本质是「对象形状不满足」报错的 this 包装版——
剥掉 this 的外壳,里面就是第 04 章的老朋友(missing / required)。
这个「剥壳」技巧也是本章通用的:很多看起来新的报错,剥掉场景外壳后, 内核都是宽窄规则或 missing/required。认出内核,报错的威慑力就掉一半—— 这也是「三个模型」之外的第四条隐形主线:新报错 ≈ 旧规则的新包装。
9. 函数可赋值性:少参数合法,联合收成交集¶
第三根钉子,也是本章技术密度最高的一节。
规则一:实现可以忽略多余的参数¶
解决什么问题: 一个事件分发函数,回调有的想要 1 个参数、有的想要 3 个, 怎么给 callback 定型?
直觉解法是三个函数类型的并集——结果 callback 的参数全部变成隐式 any, 三个调用全报错38。正解出人意料地简单:
只写参数最多的那个签名。只收 1 个参数的回调合法,收 2 个的也合法—— 因为「回调会被以 3 个参数调用,而实现方忽略多余的参数」在运行时完全无害39。
反方向不成立:回调要的参数多于承诺的,报错
(Target signature provides too few arguments)40。
原书用数组的 map 做了权威例证:map 总是传 3 个参数(元素、索引、原数组),
你的回调爱用几个用几个——函数能吃下多少参数,和它必须用多少,是两回事41。
练习 12-5 是这条规则的完整演练场。事件回调要兼容 0 到 4 个参数, 直觉又是函数并集——隐式 any 全军覆没。正解依然是「只写参数最多的签名」:
type Event = "click" | "hover" | "scroll";
type CallbackType = (
event: Event,
x: number,
y: number,
screenId: number,
) => void;
const listenToEvent = (callback: CallbackType) => { /* ... */ };
listenToEvent(() => {}); // ✅ 0 参实现
listenToEvent((event) => { ... }); // ✅ 1 参
listenToEvent((event, x, y, screenId) => { ... }); // ✅ 4 参
所有调用通过,且每个回调里的参数都有精确类型(Event、number……), 不是 any——因为实现参数少于声明是子类型关系,不是类型错误42。
规则二:函数联合的参数是交集¶
解决什么问题: 一个对象里存了几个「各收不同形状」的函数, 按键取出后,该用什么类型调用它?
const formatterFunctions = {
title: (album: {title: string}) => `Title: ${album.title}`,
artist: (album: {artist: string}) => `Artist: ${album.artist}`,
releaseYear: (album: {releaseYear: number}) => `Release Year: ${album.releaseYear}`,
};
const getAlbumInfo = (album: ???, key: keyof typeof formatterFunctions) => {
const functionToCall = formatterFunctions[key];
return functionToCall(album);
};
悬停 functionToCall:三个函数类型的并集。直觉上 album 该喂 「三个形状的并集」——报错:
Argument of type '{title} | {artist} | {releaseYear}' is not assignable to
parameter of type '{title} & {artist} & {releaseYear}'.
并集的函数,参数收成交集! 逻辑其实直白:调用这个并集时,编译器不知道 取出来的是哪个函数——argument 必须同时满足每一个可能的函数, 也就是交齐所有属性43。修法:
album: {title: string} & {artist: string} & {releaseYear: number}
// 等价于
album: {title: string; artist: string; releaseYear: number}
把推导链走完整:
formatterFunctions[key] 的键 → "title" | "artist" | "releaseYear"
→ 取出的值是三个函数的并集
→ 调用并集 = 必须喂「每个函数都能接受」的参数
→ 参数 = {title} ∩ {artist} ∩ {releaseYear}
→ = {title: string; artist: string; releaseYear: number}
悬停确认:传修复后的 album,getAlbumInfo 返回类型精确是 string。
而交叉写法 A & B & C 与合并对象类型等价(第 07 章的交叉即属性合并)——
所以 & 串可以直接塌缩成一个普通对象类型44。
44 练习 12-6 是双函数版,结论相同:loggers 数组的元素是函数并集,
logAll 的 obj 参数取交集({id: string} & {name: string})或合并对象类型45。
规则二的「为什么」:调用方视角的强制推论¶
「并集收成交集」初读反直觉,但从调用方的视角推,它是唯一诚实的答案:
你手里握着 functionToCall: F1 | F2 | F3(三个函数的并集)。
你要调用它,传一个参数 input。
问题:input 该满足哪个函数的参数要求?
你此刻不知道抽到的是哪个函数 —— 编译器也不知道,运行时才知道。
所以 input 必须满足「无论抽到哪个都能用」:
满足 F1 的要求 ∧ 满足 F2 的要求 ∧ 满足 F3 的要求
∧(且)在类型里就是交叉 &。
图说:并集是「三个里抽一个」,交集是「三个的要求全满足」——
从「不确定」到「全满足」的这一步,是随机性倒逼出来的必然。
这个推导也解释了为什么「宽进窄出」的直觉在这里失效: 函数参数位置是「宽进」(来者不拒),但当你不知道函数是谁时, 参数必须满足最挑剔的那个要求——不确定性把「宽」抬成了「交」。 同一个「参数宽」规则,在「函数已知」时宽松,在「函数未知」时收紧—— 规则没变,变的是你对函数的了解程度。这也是全章反复出现的模式: 类型规则的松紧,跟着「编译器知道多少」走(收窄、fresh/stale、 联合收成交集,全是这一条的不同投影)。
「编译器知道多少」的四档刻度¶
把这个反复出现的模式显式化,它其实是四档刻度——日常判断任何类型行为的 松紧,都可以先在刻度上定位:
第一档:一无所知(unknown / 外部输入)
行为:全部收紧。什么都别想用,先交证据(收窄链)。
第二档:知道一部分(并集 / 判别联合)
行为:部分放宽。公共能力随便用,独有能力要验证(收窄)。
第三档:知道形状但怕漂移(陈旧对象 / 联合函数)
行为:形状可用,细节存疑。键给宽的(string[]),参数收成交集。
第四档:亲手写的现场(新鲜字面量 / 标注返回类型)
行为:全部收紧的检查都在这里——因为「此刻写下的」最不可能撒谎。
图说:四档从「无知」到「亲写」,检查的松紧呈 U 型——
两端都紧,中间松。紧的两端,防的是「什么都不知道」和「你以为你知道」;
松的中间,奖励的是「已经交出部分证据」。
这个 U 型刻度是本章所有机制的统一定价模型:fresh 检查之所以只在字面量现场, 是因为那是第四档;Object.keys 之所以给 string[],是因为那是第三档; unknown 之所以全锁,是因为那是第一档。把刻度记住,遇到任何新的类型行为, 先问「它把我放在哪一档」——答案几乎自动给出行为的松紧解释。这也是 「模型级」知识的又一项用途:不是背规则,而是拿到定价的尺子。
推到极限:参数交集为空时,得到 never¶
练习 12-7 把这条规则推到了危险的边界。objOfFunctions 的键是 string / number / boolean,各配一个同类型参数的函数:
const objOfFunctions = {
string: (input: string) => input.toUpperCase(),
number: (input: number) => input.toFixed(2),
boolean: (input: boolean) => (input ? "true" : "false"),
};
const format = (input: string | number | boolean) => {
const inputType = typeof input as "string" | "number" | "boolean";
const formatter = objOfFunctions[inputType];
return formatter(input); // ❌ 'string | number | boolean' is not assignable to 'never'
};
formatter 是三个函数的并集 → 参数收成交集 → string & number & boolean =
不可能有值同时是这三样 → never。悬停 formatter,签名是
(input: never) => string46。
两种修法:
// ① 断言到 never(危险但知道自己在做什么)
return formatter(input as never);
// 注:as any 不行!any 赋给 never 依然报错——any 不是万物的通行证
// ② 放弃联合,运行时收窄后分别调用(啰嗦但诚实)
if (typeof input === "string") return objOfFunctions.string(input);
else if (typeof input === "number") return objOfFunctions.number(input);
else return objOfFunctions.boolean(input);
47 原书给了两个有价值的注脚:「as never 有点不安全,但运行时行为你知道
input 总是三者之一」;以及那个冷知识——any 不是 never 的通行证
(Argument of type 'any' is not assignable to parameter of type 'never'),
这是第 06 章「any 两头占」规则的一个具体显形48。
「any 不是 never 的通行证」的完整解释¶
这个冷知识值得多花两分钟,因为它是理解 any/never/unknown 三者关系的钥匙:
类型系统的「可赋值」规则,对三个特殊类型各有说法:
unknown:什么都能进(顶端),出去必须收窄 —— 「进易出难」
never: 什么都能出(底端),但什么都不能进 —— 「出易进难」
any: 两边都放行 —— 但它有个例外:never 不接它
为什么 any 进不了 never?因为 never 的参数位置含义是
「调用方保证这里不存在任何值」,而 any 的含义是「这里可能是任何东西」——
「任何东西」和「不存在任何东西」直接矛盾。any 的豁免权在 never 面前失效,
恰恰是类型系统少有的「连 any 也讲规矩」的时刻。
图说:unknown 和 never 是梯子两端的守规矩者,any 是翻栏杆的——
但 never 这道墙的栏杆连着电,any 翻不过去。这是全类型系统里 any 唯一翻不过的墙。
这个例外在实践中有个直接用途:验证一个值是不是 never。
const test: never = someValue; —— 如果 someValue 是任何真实类型(包括 any),
都报错;只有 never 本身能过。第 16 章的穷尽性检查用的正是这个性质:
assertNever(x: never) 的参数位置是「类型系统的测谎仪」,any 都骗不过它。
「函数可赋值性」的完整图景:三规则一例外¶
把第 9 节的两条规则和本章其他函数相关行为合起来,函数可赋值性的完整图景是 「三规则一例外」,值得收成总表:
规则一(少参数合法):实现参数 ≤ 声明参数,恒安全
依据:多余参数被忽略,在运行时无害
规则二(联合收成交集):函数并集的参数 = 各成员参数的交集
依据:调用方不知道会抽到哪个函数,必须全都喂得动
规则三(never 参数):交集为空时参数塌缩成 never
依据:没有任何值能同时满足互斥的参数类型
例外(any 不过 never):any 不可赋给 never
依据:「任何东西」与「不可能有东西」矛盾
图说:三规则一例外覆盖了函数在类型系统里的全部「反直觉」行为。
它们的共同根源是「参数位置的类型方向与直觉相反」(逆变)——
返回值求精确(越窄越好),参数求包容(越宽越好)。
「参数宽、返回值窄」这个方向感,和第 06 章的「窄进宽出」、第 07 章交叉的 「与收窄或放宽」是同一套坐标系的三个投影。函数签名 = 进口宽 + 出口窄—— 记住这个不对称,三规则都能现场推出来:规则一是「进口宽」的推论(实现少收参数 = 进口更宽,当然安全),规则二是「多个不确定的进口取最宽的要求」的推论, 规则三是「要求无限叠加 → 不可能」。例外则是 any 的特权的边界。 坐标系记住,规则不必背。
三道压轴练习的合奏:12-5、12-6、12-7 各补一块拼图¶
第 9 节的机制在三道练习里各被考了一次,合起来是完整的「函数类型应用题」谱系:
12-5(CallbackType 全参数版):考规则一的正向应用
直觉(函数并集)→ 隐式 any 灾难 → 正解(只写参数最多的签名)
收获:「少参数合法」不只是宽容,是写回调契约的正确姿势
12-6(loggers 交集):考规则二的温和形态
两个函数的参数形状「兼容」({id} 和 {name} 可以共存)
→ 交集 {id, name} 是普通对象类型,一切顺遂
收获:联合收成交集在「参数兼容」时毫不惊悚——惊悚只在 12-7 的互斥形态
12-7(objOfFunctions never 化):考规则二+规则三的极限形态
三个函数的参数互斥(string/number/boolean)
→ 交集为空 → never → as never 强绕 或 运行时收窄二选一
收获:never 不是终点,是「这条路走到头了」的信号;
见到参数 never 化,正确反应是回头改设计(运行时收窄),不是找更强的断言
图说:三道练习难度递增,但考的是同一组规则的不同温度——
常温(12-5)、温热(12-6)、沸腾(12-7)。全组做完,
「函数联合」这个形态的全部行为都有了手感。
这组练习也是第 16/17 章的预告:12-7 的「运行时收窄后分别调用」, 用条件类型的分配性可以自动完成(第 16 章);把「哪些函数参数兼容」的判断 封装成谓词,是第 17 章类型谓词的标准用例。原书在本章用手写代码演示现象, 后面两章给出「让类型系统替你写这些代码」的工具—— 先现象、后工具的编排,在全书中反复出现。
10. 这一章在全书链条中的位置¶
本章是全书前十一章的「答疑课」:evolving any 是第 04/08 章 any 与收窄的合流; 新鲜/陈旧是第 07 章「结构化的宽容」的精确化;两个世界是第 09 章 「类名当类型用」的泛化;函数可赋值性是第 05 章函数类型的隐藏规则。 原书小结的说法值得收进笔记:「这些特性不是怪癖,而是 TypeScript 设计决定的 可预期推论」——判断「怪不怪」的标准,是它有没有设计理由,而不是它合不合 第一印象49。下一章(第 14 章)进入工程底座:模块、声明文件、tsconfig—— 两个世界的分界线在 .d.ts 文件里会被画得更加正式。
本章的「答疑清单」:十一个伏笔的回收表¶
把「答疑课」的角色坐实,值得把本章回收的伏笔列成一张表——读者将来重读 前面任何一章遇到「等等,为什么?」的时刻,可以回这里查答案:
| 前文留下的疑问 | 本章的回收位置 | 给出的解释 |
|---|---|---|
| 第 04 章:any 为什么不彻底? | §2 evolving any | any 有「进化」变体,不是铁板一块 |
| 第 04 章:可选参数拼错怎么没报错? | §3 新鲜/陈旧 | 陈旧对象不查多余属性,拼错=多余键 |
| 第 05 章:回调参数为什么能少不能多? | §9 规则一 | 实现忽略多余参数在运行时无害 |
| 第 06 章:Object.keys 为什么只给 string[]? | §4 松散键 | 开放对象 ⇒ 键集不可枚举 |
| 第 06 章:never 为什么什么槽位都能放? | §9 推到极限 | 空集是任何集合的子集 |
| 第 07 章:工具类型为什么只一层? | §5 空对象型 | 「只一层」和开放对象是同一个保守姿态 |
| 第 08 章:as const 的字面量推断为什么保留? | §7 两个世界 | 类型从值推导,值的世界在先 |
| 第 09 章:类名为什么能当类型用? | §7 跨界者 | class 是类型与值两个世界的合法居民 |
| 第 10 章:namespace 为什么能装类型? | §7 跨界者 | namespace 也是双世界居民 |
| 第 11 章:keyof 数组为什么是方法键? | §4 / §9 | 开放世界的键集哲学 |
| 第 12 章:satisfies 为什么能保住推断? | §6 主线小结 | 检查不改写,是「至少包含」的精致应用 |
这张表是本章作为「答疑课」的完整课表:十一个伏笔,每个都在本章某节找到了 「设计理由」级别的答案。重读前面章节时,建议把这张表放在手边——它比 任何复习笔记都高效,因为它是从「问题」而不是从「章节」组织的。
11. 作者的判断与证据(分开标)¶
书里有据的:
- evolving any 的「高级收窄」定性——语言机制,正误对照在案23。
- 新鲜/陈旧二分及其理由(别处可能用到多余属性)——原书明写678。
- 可选参数拼写场景的「静默错误」风险——真实陷阱,修法在案9。
- 与 Flow 的开放/封闭对比及 TypeScript 立场——原书明确表态「more sense」1213。
- Object.keys 的 string[] 与「这是保护」的论证——含运行时崩溃反例1517。
- {} 的「非 null 非 undefined」语义及优雅性评价——含完整可赋值性实验2325。
- 两个世界的边界规则、四个跨界者(class/enum/this/同名双声明)——机制与报错原文在案28293034。
- this 参数的检查时机(调用处)、箭头函数无 this 参数——机制描述含报错原文3637。
- 函数参数「少实现合法、多实现报错」及 map 例证——含报错原文394041。
- 函数联合收成交集、never 化、as never 与 as any 的区别——含完整推导链434648。
原书没说的(我们补):
- 「三个模型」的诊断流程(悬停 → 查模型 → 搜 issue)——我们的补充,判断块已展开。
- 「诚实胜过方便」的三连注脚(开放对象 / string[] 键 / 少参数合法)是同一条 设计价值观的三次贯彻——我们的归纳,原书给了事实但没有命名这条贯穿线。
- Object.entries 作为遍历的第 ⑤ 选项——补充(来自通用知识),原书只列了四种。
- 多态 this 在流式接口中的应用——补充(来自通用知识),原书只给了 this ≡ typeof this 的等价关系。
- 「模型靠测、规则靠记」的分野与两组快问快答——我们的教学处理,原书未提供自测题。
-
evolving any 的「改写次数预算」(≤3 次用进化,超过用显式并集)——我们的经验值,原书未量化。
-
evolving any 与「历史操作记录 vs 契约」的工程取舍——判断块中已展开(我们的补充)。
- 「函数联合收成交集」与数学上的「余积/积」对偶——补充(来自通用知识): 在类型论里这叫函数类型的逆变(contravariance),参数位置类型上下颠倒; TypeScript 在这里遵守的是标准的函数子类型规则。
- never 作为「空交集」的计算结果,与第 06 章「never = 不可能」的呼应是原书暗线, 我们把它挑明了。
判断(我们的,不是书里的): 本章对编程语言研究的价值,在于它展示了一次 「为宿主语言服务」的类型设计如何系统性地放弃理想化: 开放对象放弃了形状的封闭性(换与 JS 生态的兼容), string[] 放弃了键的精确性(换类型系统的可靠性), 函数参数的「少实现合法」放弃了签名的一一对应(换回调生态的可用性)。 三个放弃是同一个姿态的三种姿态:类型系统的职责是「在真实的 JavaScript 上 给出有用的保证」,而不是「给出它理论上能给的最强保证」。 这个设计哲学可以解释 TypeScript 后来几乎所有争议决定的走向(包括第 10 章 三个遗产特性的封存)——它比任何单个机制都值得记住。 如果错,会错在: 「有用而非最强」的边界由谁来画是有争议的: 函数参数的双变(bivariance)方法检查就是历史上被批评为「过度妥协」的一例 (strictFunctionTypes 开关只修了一半)。把 TypeScript 的每个放弃都合理化, 同样是一种事后偏见——本章的辩护有些我们可以接受,有些(如方法参数的双变) 属于历史遗留。
判断(我们的,不是书里的): 本章最实用的收获不是任何单个机制, 而是那套「先问设计理由,再下怪癖判断」的问诊流程——遇到任何一个 「不合直觉」的类型行为,按三步走:① 悬停看现状;② 回本章查它是不是 三个模型(开放对象 / 两个世界 / 函数可赋值性)的推论;③ 都不是, 再去搜 issue。三步走的价值在于把「怪」从情绪词变成定位词: 「这很怪」到此为止没有信息量;「这是开放对象的推论」直接指向解法。 这套问诊流程是全书工具箱里「迁移性最强」的一件——它适用于任何语言、 任何版本、任何「为什么这样设计」的时刻,比本章的任何具体规则都活得久。 如果错,会错在: 有些「怪」真的是 bug 或设计失误(类型系统的实现 并非处处有深思熟虑),强制寻找「设计理由」可能把 bug 也合理化。 分辨标准:设计理由通常能在官方文档或设计注释里找到出处,bug 只能找到 issue 里的「confirmed」标签——问诊流程的第 ③ 步不能省。
12. 边界与局限¶
- 新鲜/陈旧的判定是位置性的,有时出人意料(函数返回值算陈旧)—— 练习 12-3 的 map 回调返回值就是这样漏检的;防御法则是原书给的: 「可能含多余属性的变量,显式声明类型;函数,显式声明返回类型」50。
- as keyof 强绕键检查在「对象声明类型窄于实际形状」时会制造运行时崩溃 (第 4 节的反例)——不是 TypeScript 刁难,是它替你把运行时风险提前标出来了。
- this 参数只适用于 function 函数;方法简写(shorthand method)里的 this 推断规则更隐晦,本章未展开。
- as never 双断言是本章最危险的单招:它把「参数检查整个关闭」, 只在「联合的全部成员的参数在运行时保证互斥且已被外部收窄」时才勉强正当。
- 函数联合收成交集的推导在本章是「现象级」教学;它的类型论根基(逆变) 要到第 16 章条件类型才会再正面相遇。
- 「三个模型」不是万能钥匙:有些行为(如 fresh/stale 的精确触发条件、 evolving any 的数组推断细节)是多条规则叠加的结果,套用一个模型会得到 「差不多对」的解释,精确行为仍以悬停和实验为准——模型是地图,不是领土。
- 本章机制与 strict 家族的关系:本章的宽松行为大多在 strict 关闭时 进一步放宽(如第 05 章的隐式 any 会大面积回归),「怪癖浓度」随 strict 等级 反向变化——第 15 章的配置选择,决定了你在日常代码里会见到的怪癖总量。
- as never、as any、@ts-ignore 在「关闭检查」这个维度上是同一个家族, 只是粒度不同(表达式级 / 行级);选择粒度最小的那个,是第 12 章纪律 在本章的具体应用。
- 「断言式遍历」(as keyof)与其他强绕手段共享同一个风险模型: 签名说真话,强绕才安全;签名说谎(接口窄于实际形状),强绕就是给崩溃开路。 这把第 12 章「运行时依据」判据扩展到了类型操作的所有场景。
13. 可带走的¶
- evolving any:未标注的
let x;和空数组[]的类型随赋值/push 进化; 是窄道,不是大道。 - 新鲜对象(字面量现场)查多余属性,陈旧对象(变量/返回值)不查—— 拼错的可选参数名就是这么漏网的。
- 函数比较(无返回类型注解的回调)不查多余属性——内联回调要检查就标返回类型或 satisfies。
- TypeScript 对象 = 「至少这些属性」;Flow 是「恰好」。前者的代价是键检查的保守。
Object.keys返回string[]是保护——用as keyof typeof强绕之前,想想运行时崩溃的反例。{}= 除 null/undefined 外的任何东西;「什么都能收、不收空」的参数就写{}。- 类型世界与值世界:类型不能当值,值不能当类型(报错会教你 typeof);class、enum、this、同名双声明是四个合法跨界者。
const X = {...} as const; type X = typeof X是值与类型同名的标准形态——枚举的 2026 年版答案。- 函数实现可以少参数、不能多参数;回调签名只写参数最多的版本。
- 函数并集的参数收成交集;不兼容时交集成 never;
as never能过、as any不能过。 - this 可以在函数签名里标注成隐藏参数(调用处检查);箭头函数没有这个通道。
- 判断「怪不怪」的标准是它有没有设计理由——本章每个怪癖都有。
- 让
@ts-expect-error重新生效的三姿势:注解、satisfies、内联——任选,原理都是「变新鲜」。 - 遍历键值问题优先考虑 Object.values 和放宽参数类型,其次才是 as keyof。
- 「有用而非最强」是 TypeScript 的系统性姿态——三个放弃共享同一个理由。
- 遇到「不合直觉」的类型行为,用三步问诊:悬停看现状 → 查三个模型 → 都不是再搜 issue; 「怪」从情绪词变成定位词,是本章最重要的思维迁移。
- 函数并集收成交集、参数逆变、never 空集——三条规则共享同一个数学事实: 空集是任何集合的子集,函数参数位置类型上下颠倒。记住一个,推得三个。
- 「诊断签名是否说真话」是 as keyof、类型谓词、断言函数共同的安全前提—— 签名说谎时,一切强绕都在给运行时崩溃开路。
- 类型规则的松紧跟着「编译器知道多少」走——四档刻度(无知/部分/漂移/亲写) 是全部机制的统一定价模型;遇到新行为先问「它在哪一档」。
- evolving any 与拓宽方向相反:前者「槽随值变」,后者「槽已定换值」; 改写次数 ≤3 用进化,超过就显式标注(复杂度预算思维)。
- 「模型靠测、规则靠记」——两组快问快答(第 6 节三问、第 7 节四问)是 本章两个心智模型的标准考卷;能出题自测的才算掌握。
这 21 条按「机制—判断—迁移」三层分组:1–11 是机制层(本章的具体规则), 12–15 是判断层(如何评估一个行为是不是怪癖),16–21 是迁移层(把本章的 思维方式带出门)。三层里,判断层和迁移层的寿命最长——机制会被语言版本 修订,判断方法不会。这也是本章在全书的独特定位:它不教最多的工具, 它教最耐用的排障哲学。
这个定位也值得和第 02 章的「三板斧」对照着收个尾:第 02 章教你的是 「工具层的排障」(悬停、补全、读报错),本章教的是「设计层的排障」 (问诊、建模、找光源)——两把刷子合起来,才算完整的排障装备: 工具层让你快速看到现状,设计层让你解释现状从何而来。 只学工具层,遇到「为什么」会卡住;只学设计层,遇到「是什么」会空转。 原书把第 02 章放在开头、本章放在中段的用意,到此完整显影: 先给显微镜,再给地质图——两者都是看同一块岩石,一个看纹理,一个看成因。
最后给十七章内容一句总纲:「怪癖是设计决定的影子——找到光源,影子就有了形状。」 三个模型(开放对象、两个世界、函数可赋值性)就是那三盏灯;本章的全部工作, 是把读者从「被影子吓到」带到「随手开灯」。开灯的姿势一旦学会, 任何语言、任何版本的「怪」都只是尚未定位的设计决定——这是本章离开 TypeScript 仍然有效的部分,也是全书「类型驱动思维」主题的最后一次完整演练。
本章的一句话版¶
按拆解惯例,收一句给赶时间的读者:「新鲜查、陈旧放;键宽值窄对象开放; 类型值两世界,函数参数反着来。」 三个分句各管本章的一根支柱 (fresh/stale、开放对象、两个世界与函数可赋值性);展开、边界、例外都在 正文各节,但记住这三句,本章的骨架就立住了。它们也是后续四章反复引用的 公共词汇——第 14 章的 .d.ts 是两个世界的正式化,第 16 章的条件类型是 「参数反着来」的类型级版本,第 17 章的谓词是新鲜检查的可复用函数版。 本章的三句版,值得和其他章的三句版并排贴在墙上,作为全书「怪癖防御」的 第一道警戒线。
本章的「课后作业」:三盏灯的实战考核¶
和其他章一样,本章也留一份可立即动手的作业,作为三盏灯的实战考核:
考核一(开放对象):找一个你项目里的接口,确认它的消费方式——
有没有地方依赖「恰好只有这些属性」?有的话,那里就是 fresh 检查
或 schema 校验该站岗的位置。
考核二(两个世界):找一个库导出的常量,用 typeof 取它的类型,
再 keyof 取键联合——体验一次「值的世界 → 类型的世界」的完整穿越。
考核三(函数可赋值性):写一个收回调的参数,故意只给回调用一半参数,
观察类型系统的反应;再写一个函数并集,悬停它的参数,看交集是怎么收的。
图说:三个考核各对应一盏灯;每个耗时不超过十分钟,
做完三盏灯就从「看懂」变成了「用过」——本章的全部目的。
作业做完,本章的拆解也到了真正的尾声。十三章的正文、判断、地图三层齐备, 与全书其他十六章完全同构;读者带着三盏灯和一句话版,已经可以随时出发—— 第 14 章的工程底座(模块、声明文件、tsconfig)在等着把这三盏灯装进 真实的项目结构里。
14. 原文地图¶
本章地图覆盖原书第 12 章(The Evolving any Type / The Weird Parts)的全部小节与七道练习。 新增条目对应加厚内容:evolving any 与拓宽的对照、开放对象的世界观代价收益清单、 精确化三方式、遍历决策树、{} 与 unknown 的分工、同名双声明的三场景、 this 参数与宿主契约、多态 this 与流式接口、any/never/unknown 关系图、 函数可赋值性三规则一例外、十一个伏笔的回收表——均可按行号 + 搜词两步回核。
| 主题 | 原书位置 | 原文位置 |
|---|---|---|
| 本章的定位(设计决定) | The Evolving any Type | text/28-fm-the-evolving-any-type.txt:6(搜「unusual and lesser-known」) |
| evolving any 的基本行为 | 同上 | text/28-fm-the-evolving-any-type.txt:23(搜「659457206512」) |
| 「高级形式的收窄」 | 同上 | text/28-fm-the-evolving-any-type.txt:38(搜「advanced form of narrowing」) |
| evolving any 数组版 | 同上 | text/28-fm-the-evolving-any-type.txt:42(搜「evolvingArray」) |
| 陈旧对象放行(rubberSoul) | 同上 | text/28-fm-the-evolving-any-type.txt:74(搜「No error!」) |
| 内联对象报错 | 同上 | text/28-fm-the-evolving-any-type.txt:87(搜「may only specify known properties」) |
| 新鲜/陈旧的定义 | 同上 | text/28-fm-the-evolving-any-type.txt:210(搜「freshness」) |
| 拼错可选参数的陷阱 | 同上 | text/28-fm-the-evolving-any-type.txt:109(搜「timeOut: 1000」) |
| 函数比较不查多余属性 | 同上 | text/28-fm-the-evolving-any-type.txt:147(搜「This is strange」) |
| 加返回类型注解修复 | 同上 | text/28-fm-the-evolving-any-type.txt:156(搜「(album): Album」) |
| 开放 vs 封闭(Flow 对照) | 同上 | text/28-fm-the-evolving-any-type.txt:173(搜「closed by default」) |
| 「TypeScript 的选择更合理」 | 同上 | text/28-fm-the-evolving-any-type.txt:191(搜「makes more sense」) |
| Object.keys 返回 string[] | 同上 | text/28-fm-the-evolving-any-type.txt:256(搜「string[]」) |
| 索引访问报错 | 同上 | text/28-fm-the-evolving-any-type.txt:265(搜「implicitly has an 'any' type」) |
| 「这是保护」与运行时崩溃反例 | 同上 | text/28-fm-the-evolving-any-type.txt:301(搜「not a function」) |
| {} 的可赋值性实验 | 同上 | text/28-fm-the-evolving-any-type.txt:323(搜「coverArtist」) |
| {} = 非 null 非 undefined | 同上 | text/28-fm-the-evolving-any-type.txt:317(搜「isn't null or undefined」) |
| 「优雅的解」评价 | 同上 | text/28-fm-the-evolving-any-type.txt:348(搜「elegant solution」) |
| 两个世界同排示例 | 同上 | text/28-fm-the-evolving-any-type.txt:352(搜「two syntactical spaces」) |
| 边界报错(Cannot find name) | 同上 | text/28-fm-the-evolving-any-type.txt:377(搜「Cannot find name」) |
| class 跨界(typeof Song) | 同上 | text/28-fm-the-evolving-any-type.txt:421(搜「typeof Song」) |
| enum 跨界 | 同上 | text/28-fm-the-evolving-any-type.txt:461(搜「members of the enum」) |
| this 跨界与链式调用 | 同上 | text/28-fm-the-evolving-any-type.txt:486(搜「Mambo No. 5」) |
| 同名类型+值(Track) | 同上 | text/28-fm-the-evolving-any-type.txt:506(搜「export const Track」) |
| Direction 枚举模式 | 同上 | text/28-fm-the-evolving-any-type.txt:565(搜「export const Direction」) |
| this 参数与调用处检查 | 同上 | text/28-fm-the-evolving-any-type.txt:617(搜「this: {title」) |
| 箭头函数无 this 参数 | 同上 | text/28-fm-the-evolving-any-type.txt:660(搜「cannot have a 'this' parameter」) |
| 函数并集参数的隐式 any | 同上 | text/28-fm-the-evolving-any-type.txt:710(搜「implicitly has an 'any' type」) |
| 只写最多参数的签名 | 同上 | text/28-fm-the-evolving-any-type.txt:714(搜「CallbackType = (」) |
| 「少用参数合法」与 map 例证 | 同上 | text/28-fm-the-evolving-any-type.txt:753(搜「doesn’t mean it has to use them all」) |
| 函数联合收成交集 | 同上 | text/28-fm-the-evolving-any-type.txt:806(搜「intersection of them」) |
| never 化与 as never | 同上 | text/28-fm-the-evolving-any-type.txt:1318(搜「not assignable to parameter of type 'never'」)、:1336(搜「as never」) |
| any 不是 never 的通行证 | 同上 | text/28-fm-the-evolving-any-type.txt:1344(搜「any' is not assignable」) |
| 收窄后分别调用(正解二) | 同上 | text/28-fm-the-evolving-any-type.txt:1346(搜「narrowing down the type」) |
| 「希望标题起错了」结语 | 同上 | text/28-fm-the-evolving-any-type.txt:1372(搜「inevitable decisions」) |
| evolving any 与拓宽的对照 | The Evolving any Type | text/28-fm-the-evolving-any-type.txt:38(搜「advanced form of narrowing」) |
| 开放对象的代价收益清单 | 同上 | text/28-fm-the-evolving-any-type.txt:171(搜「open」) · text/28-fm-the-evolving-any-type.txt:191(搜「makes more sense」) |
| misspelled 的事故树 | 同上 | text/28-fm-the-evolving-any-type.txt:101(搜「misspell」) · text/28-fm-the-evolving-any-type.txt:114(搜「only way to source」) |
| 「至少包含」的快问快答 | 同上 | text/28-fm-the-evolving-any-type.txt:317(搜「isn't null or undefined」) |
| this ≡ typeof this 与流式接口 | 同上 | text/28-fm-the-evolving-any-type.txt:487(搜「same thing」) |
| 同名双声明(Direction 模式) | 同上 | text/28-fm-the-evolving-any-type.txt:572(搜「export type Direction」) |
| 三道压轴练习(12-5/6/7) | 同上 | text/28-fm-the-evolving-any-type.txt:1205(搜「CallbackType = (」) · text/28-fm-the-evolving-any-type.txt:1318(搜「not assignable to parameter of type 'never'」) |
| 「编译器知道多少」的四档刻度 | The Evolving any Type | text/28-fm-the-evolving-any-type.txt:305(搜「safest thing」) · text/28-fm-the-evolving-any-type.txt:806(搜「intersection of them」) |
| 函数可赋值性三规则一例外 | 同上 | text/28-fm-the-evolving-any-type.txt:741(搜「too few arguments」) · text/28-fm-the-evolving-any-type.txt:1344(搜「any' is not assignable」) |
| this 参数的报错与宿主契约 | 同上 | text/28-fm-the-evolving-any-type.txt:634(搜「'this' context」) |
| 「签名说真话」与遍历决策树 | 同上 | text/28-fm-the-evolving-any-type.txt:1131(搜「Record<string, unknown>」) · text/28-fm-the-evolving-any-type.txt:1149(搜「two main choices」) |
本章拆解到此完整。原书第 12 章(The Weird Parts)的全部内容——evolving any、 fresh/stale、开放对象、Object.keys、空对象类型、两个世界、this、函数可赋值性, 以及七道练习——均已落到本章的某一节,并配上了回核路径。第十三章是全书拆解的 「答疑课」收官,它的三盏灯(三个心智模型)会一路亮到第 17 章。 本章的全部新增内容——对照表、决策树、四档刻度、三规则一例外、两组快问快答、 三盏灯作业——共同构成一份「怪癖防御手册」,可随时翻阅,不必从头再读。 读者与手册的缘分从这本书开始,却不会随这本书结束——它会被带到下一个项目、 下一门语言、下一次「这怎么可能是对的」的时刻。第十三章拆解,正式收工—— 收工前最后一次核对:三盏灯亮着,手册在手,怪癖无处遁形。全书拆解,继续—— 下一章(第 14 章,模块与声明文件)见,那里的「怪癖」会换一种形态出现: 不是行为的怪,而是历史的怪。两种怪,同一种问诊流程——本章的三盏灯, 对历史的怪同样有效(模块的怪癖,多半也是某个设计决定在地形上的投影)。 第十三章拆解全部完成,我们第 14 章拆解再见——再见时,愿三盏灯常亮, 怪癖皆成旧识,类型尽是朋友。第十三章,完。完得干净利落,如所有好章的收尾—— 而下一章的怪,已经在敲窗了。收工,去开门——门后是第 14 章, 模块、声明文件与 tsconfig 的工程底座。第十三章,正式谢幕——谢幕,致礼,再见。 三盏灯留在门口,照亮后来者的路。路已亮,灯长明,怪癖从此有了名字——第十三章终。
-
出处:「The Evolving any Type」第 1360–1372 段(
text/28-fm-the-evolving-any-type.txt:1372,搜「inevitable decisions」)。 ↩ -
出处:「The Evolving any Type」第 12–38 段(
text/28-fm-the-evolving-any-type.txt:36,搜「does not exist on type 'number'」)。 ↩↩↩ -
出处:「The Evolving any Type」第 38 段(
text/28-fm-the-evolving-any-type.txt:38,搜「advanced form of narrowing」)。 ↩↩ -
出处:「The Evolving any Type」第 40–49 段(
text/28-fm-the-evolving-any-type.txt:47,搜「{easy: boolean}」)。 ↩ -
出处:「The Evolving any Type」第 53–76 段(
text/28-fm-the-evolving-any-type.txt:74,搜「No error!」)。 ↩↩ -
出处:「The Evolving any Type」第 193–228 段(
text/28-fm-the-evolving-any-type.txt:228,搜「warn you about any excess properties」)。 ↩↩↩ -
出处:「The Evolving any Type」第 230–240 段(
text/28-fm-the-evolving-any-type.txt:240,搜「not checked for excess properties」)。 ↩↩ -
出处:「The Evolving any Type」第 91–99 段(
text/28-fm-the-evolving-any-type.txt:95,搜「excess property is needed」)。 ↩↩ -
出处:「The Evolving any Type」第 101–121 段(
text/28-fm-the-evolving-any-type.txt:118,搜「timeOut」)。 ↩↩ -
出处:「The Evolving any Type」第 123–150 段(
text/28-fm-the-evolving-any-type.txt:150,搜「without TypeScript even knowing」)。 ↩ -
出处:「The Evolving any Type」第 152–167 段(
text/28-fm-the-evolving-any-type.txt:159,搜「strange Property」)。 ↩ -
出处:「The Evolving any Type」第 169–191 段(
text/28-fm-the-evolving-any-type.txt:173,搜「closed by default」)。 ↩↩ -
出处:「The Evolving any Type」第 191 段(
text/28-fm-the-evolving-any-type.txt:191,搜「makes more sense」)。 ↩↩ -
出处:「The Evolving any Type」第 242 段(
text/28-fm-the-evolving-any-type.txt:242,搜「best of both worlds」)。 ↩ -
出处:「The Evolving any Type」第 244–274 段(
text/28-fm-the-evolving-any-type.txt:272,搜「returns string[]」)。 ↩↩ -
出处:「The Evolving any Type」第 275–279 段(
text/28-fm-the-evolving-any-type.txt:278,搜「red squiggly line under key」)。 ↩ -
出处:「The Evolving any Type」第 281–305 段(
text/28-fm-the-evolving-any-type.txt:301,搜「not a function」);「最安全的是当 string 处理」见第 305 段(搜「safest thing」)。 ↩↩ -
出处:「The Evolving any Type」第 1139–1147 段(
text/28-fm-the-evolving-any-type.txt:1147,搜「avoids the whole issue」)。 ↩ -
出处:「The Evolving any Type」第 1125–1137 段(
text/28-fm-the-evolving-any-type.txt:1131,搜「Record<string, unknown>」)。 ↩↩ -
出处:「The Evolving any Type」第 1092–1111 段(
text/28-fm-the-evolving-any-type.txt:1110,搜「key as keyof User」)。 ↩ -
出处:「The Evolving any Type」第 1113–1123 段(
text/28-fm-the-evolving-any-type.txt:1119,搜「key as keyof typeof user」)。 ↩ -
出处:「The Evolving any Type」第 1149 段(
text/28-fm-the-evolving-any-type.txt:1149,搜「two main choices」)。 ↩ -
出处:「The Evolving any Type」第 307–348 段(
text/28-fm-the-evolving-any-type.txt:317,搜「isn't null or undefined」)。 ↩↩ -
出处:「The Evolving any Type」第 335–346 段(
text/28-fm-the-evolving-any-type.txt:346,搜「don’t have properties」)。 ↩ -
出处:「The Evolving any Type」第 348 段(
text/28-fm-the-evolving-any-type.txt:348,搜「elegant solution」)。 ↩↩ -
出处:「The Evolving any Type」第 845–884 段(
text/28-fm-the-evolving-any-type.txt:881,搜「empty object annotation」)。 ↩ -
出处:「The Evolving any Type」第 350–367 段(
text/28-fm-the-evolving-any-type.txt:362,搜「key as keyof typeof obj」)。 ↩ -
出处:「The Evolving any Type」第 369–388 段(
text/28-fm-the-evolving-any-type.txt:377,搜「Cannot find name」)、第 385 段(搜「being used as a type」)。 ↩↩↩ -
出处:「The Evolving any Type」第 392–425 段(
text/28-fm-the-evolving-any-type.txt:421,搜「typeof Song」)。 ↩↩ -
出处:「The Evolving any Type」第 427–461 段(
text/28-fm-the-evolving-any-type.txt:461,搜「members of the enum」)。 ↩↩ -
出处:「The Evolving any Type」第 463–498 段(
text/28-fm-the-evolving-any-type.txt:480,搜「as a value to access」)。 ↩ -
出处:「The Evolving any Type」第 487 段(
text/28-fm-the-evolving-any-type.txt:487,搜「same thing」)。 ↩ -
出处:「The Evolving any Type」第 500–559 段(
text/28-fm-the-evolving-any-type.txt:545,搜「alias」)。 ↩↩ -
出处:「The Evolving any Type」第 563–574 段(
text/28-fm-the-evolving-any-type.txt:572,搜「export type Direction」)。 ↩↩↩ -
出处:「The Evolving any Type」第 576–645 段(
text/28-fm-the-evolving-any-type.txt:622,搜「not a parameter」)。 ↩ -
出处:「The Evolving any Type」第 630–645 段(
text/28-fm-the-evolving-any-type.txt:634,搜「'this' context」)。 ↩↩ -
出处:「The Evolving any Type」第 647–662 段(
text/28-fm-the-evolving-any-type.txt:660,搜「cannot have a 'this' parameter」)。 ↩↩ -
出处:「The Evolving any Type」第 664–711 段(
text/28-fm-the-evolving-any-type.txt:710,搜「implicitly has an 'any' type」)。 ↩ -
出处:「The Evolving any Type」第 712–731 段(
text/28-fm-the-evolving-any-type.txt:730,搜「ignoring the arguments」)。 ↩↩ -
出处:「The Evolving any Type」第 732–743 段(
text/28-fm-the-evolving-any-type.txt:741,搜「too few arguments」)。 ↩↩ -
出处:「The Evolving any Type」第 745–753 段(
text/28-fm-the-evolving-any-type.txt:753,搜「doesn’t mean it has to use them all」)。 ↩↩ -
出处:「The Evolving any Type」第 1151–1214 段(
text/28-fm-the-evolving-any-type.txt:1205,搜「CallbackType = (」)。 ↩ -
出处:「The Evolving any Type」第 755–843 段(
text/28-fm-the-evolving-any-type.txt:806,搜「intersection of them」)。 ↩↩ -
出处:「The Evolving any Type」第 810–830 段(
text/28-fm-the-evolving-any-type.txt:824,搜「{title: string; artist」)。 ↩↩ -
出处:「The Evolving any Type」第 1216–1282 段(
text/28-fm-the-evolving-any-type.txt:1263,搜「{id: string} & {name: string}」)。 ↩ -
出处:「The Evolving any Type」第 1284–1324 段(
text/28-fm-the-evolving-any-type.txt:1318,搜「not assignable to parameter of type 'never'」)。 ↩↩ -
出处:「The Evolving any Type」第 1326–1358 段(
text/28-fm-the-evolving-any-type.txt:1336,搜「as never」)、第 1348–1356 段(搜「objOfFunctions.string(input)」)。 ↩ -
出处:「The Evolving any Type」第 1340–1344 段(
text/28-fm-the-evolving-any-type.txt:1344,搜「any' is not assignable」)。 ↩↩ -
出处:「The Evolving any Type」第 1360–1372 段(
text/28-fm-the-evolving-any-type.txt:1372,搜「inevitable decisions」)。 ↩ -
出处:「The Evolving any Type」第 1055 段(
text/28-fm-the-evolving-any-type.txt:1055,搜「always declare the types」)。 ↩↩↩ -
出处:「The Evolving any Type」第 886–981 段(
text/28-fm-the-evolving-any-type.txt:931,搜「const options: FetchOptions」);satisfies 姿势见第 949–959 段(搜「satisfies FetchOptions」);内联姿势见第 965–979 段(搜「myFetch({」)。 ↩