现代工具三件套 — TypeScript、ESLint、Prettier 如何把「运行时惊吓」前移
现代工具三件套 — TypeScript、ESLint、Prettier 如何把「运行时惊吓」前移
这一章讲三件事: TypeScript 怎么在代码跑起来之前抓出类型错误;ESLint 怎么把「团队约定」 变成机器可执行的规则(以及怎么优雅地破例);Prettier 加 Husky 怎么让「格式对不对」 从人际争论变成自动发生。主线只有一个:把错误发现的时间点,从「运行那一刻」一路提前到「敲键盘那一刻」。
1. 先看现象:一个写反的参数,三种死法
原书用一个小 bug 开场,它值得完整走一遍,因为这一章的每个工具都在它的某一次「死法」上起作用。
setTimeout 是启动定时器的全局函数,正确的调用是先传要执行的函数、再传等多少毫秒:
setTimeout(() => { ... }, 1000) 表示 1 秒后执行。如果手滑把两个参数写反:
// 错误代码!时间应该放在回调之后
setTimeout(200, () => {
console.log("This will never run.");
});这段代码在两个环境里的死法不一样,而这不一样本身就是大启示:1
- 在浏览器里:静默死亡。 不抛任何错误,控制台干干净净,定时器永远不触发, 你的排障毫无线索。很多浏览器 API 都是这种「失败无声」的性格。
- 在 Node.js 里:当场报错,且信息有用——参数类型不对,运行时就抛出一个带说明的异常。
Node 的严格是件好事,但原书立刻追问了本章的主问题:为什么要等到运行时才报错? 这个错误光看代码就能预见。 历史上人们试过用别的语言(如 Google 的 GWT,把 Java 编译成 JavaScript) 整体替代;TypeScript 换了条路——它不是另一门语言,而是JavaScript 的超集,加上能在编译期验证的类型。2
「超集」的意思是:所有合法 JavaScript 都是合法 TypeScript。上面那段写反的代码原样放进 TypeScript 编译器,照样通过语法检查——但类型检查器能认出它是个错误。这一区分(语法归语法、类型归类型) 是全章的地基。
主线例子的旅程预告
同一个 bug(setTimeout 参数写反):
浏览器:无声失败 ← 最坏的死法,没有任何信号
Node:运行时抛错 ← 有信号,但已经晚了
TypeScript:编译时报错 + 精确位置 ← 第 2 节
VS Code:敲代码时波浪线 ← 编辑器集成,第 2 节末
ESLint:同类问题写成规则,全员强制 ← 第 3 节
Prettier:至少它会被排成一致的格式 ← 第 4 节
Husky:提交时自动过一遍 ← 第 4 节末2. TypeScript:给 JavaScript 装上编译期的类型检查
2.1 两个容易混的概念:动态类型 vs 隐式转换
抱怨「JavaScript 没有真正的类型」时,其实混了两件事,原书特意拆开:3
第一,JavaScript 是动态类型语言:类型只在运行时 enforced(强制执行)。
静态类型语言在编译期就拦下 把字符串传给想要数字的函数 这类错误;TypeScript 让你能在跑代码之前
先跑类型检查器,所以它属于静态类型这一边。
第二,JavaScript 有隐式类型转换:1 + "2" 得到字符串 "12",因为任何值加字符串都会被自动转成字符串。
TypeScript 的运行时行为和 JavaScript 完全一样(1 + "2" 照样得 "12"),但它的类型检查器能预判转换结果:
它知道 1 + "2" 是 string,于是当这个表达式被用在「期望数字」的地方时,它报错。4
一句话:TypeScript 不改变程序怎么跑,只改变你在哪一步发现它跑得不对。
2.2 装起来:三个命令看清「类型检查 ≠ 编译」
原书的动手路径(照抄):5
# 1. 建一个空项目
mkdir typescript-project && cd typescript-project
echo '{}' > package.json # package.json 是 npm 管依赖的账本,第 3 章细讲
# 2. 装 TypeScript(只开发用,不当运行时依赖)
npm install --save-dev typescript
# 3. 生成一份默认配置
npx tsc --initnpx tsc --init 生成的 tsconfig.json 会带上几行关键默认:strict: true、target: es2016、
module: commonjs 等。6 这个文件回答四个问题:编译哪些文件、从别处带哪些类型、检查多严格、
要不要吐出 JavaScript、吐出的目标版本是哪代 ECMAScript。
然后把主线 bug 存进 timeout.ts,跑 npx tsc,你会得到:7
timeout.ts:1:12 - error TS2345: Argument of type 'number' is not assignable to
parameter of type 'TimerHandler'.「number 不可赋给 TimerHandler」——检查器不但报了错,还精确到第 1 行第 12 列。
这就是 TypeScript 的核心体验:错误消息里的词汇,就是你代码里的词汇。
2.3 一个反直觉的事实:报了错,它照样编译
注意上面流程的细节:报完错,tsc 并没有停下——它照旧吐出一个 timeout.js,内容几乎和源码一样
(只多了首行 "use strict")。类型错误不阻塞编译,这是新手第一个要适应的行为。8
背后的原则是:TypeScript 编译器默认只做两件「减法」——剥掉类型注解、把新语法翻译成目标 ECMAScript 版本;
它不往你的代码里加任何东西("use strict" 是少数例外,由 strict 选项连锁打开)。
原书强调,这种「零运行时成本」正是 TypeScript 成功的大原因:类型保证不花任何代码体积和性能的代价。9
只想检查不想出文件?用 npx tsc --noEmit。现代构建工具(如 Vite)接管编译时常用这个模式,
类型检查变成一条独立流水线。
2.4 隐式 any:检查器什么时候「举手投降」
回到主线旅程的下一步。再看一个无害的函数:10
const pluralize = (str, count) => (count === 1 ? str : `${str}s`);纯 JavaScript,毫无问题。跑 tsc --noEmit,却报两个错:Parameter 'str' implicitly has an 'any' type。
原因是:类型检查器会为每个变量推断类型——let x = 5 推断 x 是数字;
addEventListener("keydown", (event) => ...) 从注册位置推断 event 是键盘事件。
但它只看声明处的上下文,不看别处。(str, count) 这对接在箭头函数头上的参数,
没有任何上下文线索,检查器「摊开手放弃了」——退回到一个特殊类型:any。11
any 是 TypeScript 里的「检查关闭阀」:变量一旦被标成 any,对它的一切检查都失效,
它能被赋给任何东西、任何东西也能赋给它。迁移老代码时它是逃生门,平时是隐患,
所以 strict 模式规定:隐式落到 any 一律报错(这条由 noImplicitAny 控制;想连显式写 any 也禁掉,
有 ESLint 规则 no-explicit-any)。
解法是给检查器「更多线索」——类型注解:
const pluralize = (str: string, count: number) => count === 1 ? str : `${str}s`;注解加上后,检查器不但能拦住函数体内的类型错,还能推断出返回值是 string——count === 1 分支返回
string,模板串分支也返回 string,合起来就是 string。这就是「你给的线索越多,它能帮的越多」这条全书反复出现的规律。12
复杂类型不必挤在参数里,可以单独起名字(type declaration,类型声明):
interface StarshipOptions {
captain: "Kirk" | "Picard" | "Janeway"; // 联合三个具体值,读作「三选一」
warpFactor: number;
engage?: boolean; // 问号 = 可选:可以不传
}
const startStarship = (options: StarshipOptions) => { /* ... */ };这里一次出现了三样小机制,都在原书这段代码里:interface 是给对象类型起名的关键字;
竖线 | 是联合类型,把几个类型并成一个;键名后的 ? 表示可选,可以省略。
声明出的类型和函数一样遵守作用域规则,可以 export 给别的模块用。13
(对象类型用 interface 还是 type 关键字?官方手册的说法是:大部分情况随喜好,
拿不准就用 interface 直到需要 type 独有的功能。原书照此执行。14)
2.5 类型收窄:检查器怎么「跟着你的 if 走」
类型收窄(type narrowing) 是 TypeScript 最有「跟得住代码」味道的特性。看这个函数:15
interface Mage { magic: number }
interface Thief { dexterity: number }
const rerollAttributes = (character: Mage | Thief) => {
if ("magic" in character) {
character.magic = Math.ceil(Math.random() * 20); // 检查器允许:这里一定是 Mage
}
if ("dexterity" in character) {
character.dexterity = Math.ceil(Math.random() * 20);
}
};character 被标成 Mage | Thief(联合)。直接写 character.magic = ... 会报错
——万一是 Thief 呢?但 "magic" in character 是运行时检查(这个对象上有没有这个属性),
检查器明白:走进了 if 里面,character 就不可能是 Thief 了,于是在这个分支里把类型收窄成 Mage。16
这种「看行为猜类型」的思路有个形象的名字:鸭子类型(duck typing)——「如果它走起来像鸭子、
叫起来像鸭子,那它就是鸭子」。来自强类型语言的开发者第一反应是写
if (character instanceof Mage)——这在 TypeScript 里是错误:Mage 只是个类型,
类型在运行时根本不存在,instanceof 操作的对象必须是运行时的值。检查器会报:
「'Mage' only refers to a type, but is being used as a value here」。17
这句报错是理解 TypeScript 哲学的一把钥匙:类型是给检查器看的,编译后全部擦除。
可以直接验证:把上面代码编译,产物里只剩 in 检查和赋值,interface 和类型注解消失得无影无踪。18
收窄的手法有很多种,书里给了「巡游」:跟具体值比较(=== 23)、typeof 查原始类型、
Array.isArray 查数组,以及 satisfies 关键字在原地断言「这一行它的类型应该已经是某某」。19
收窄的两个天花板(原书明说的局限):20
- 对可变结构(对象、数组)的后续修改,检查器常常跟不上:数组
["str", 123]的类型被推断成(string | number)[],于是mixedArray[0]的类型是string | number,哪怕你从未往里放过别的。 解决:字面量后加as const,锁成只读元组,mixedArray[0]就能精确到"str"。 - 检查器不会跨函数调用做收窄。
if (isString(x))能收窄,因为检查器对 「返回 true ⟹ 参数是 string」这类函数有特判;但if (isShortString(x))(内部是typeof x === "string" && x.length <= 3)不能,因为它推不出反方向 「返回 false ⟹ 参数不是 string」——不满足它的特判条件,于是一次推断都不做。
第二条天花板有解药,但解药有副作用:类型谓词(type predicate)。
const isShortString = (x: unknown): x is string => {
return typeof x === "string" && x.length <= 3;
};: x is string 这个注解强制检查器接受「true ⟹ string」。问题是它同时强制了反方向:
传进一个 "long string",检查器会推出 else 分支里它是 never(不可能的类型)——
因为「不是 string」+「已知是 string」= 矛盾。21 更安全的兜底是 as 关键字:
「关于这个变量的类型,我比你知道得多」——但原书警告:检查器只和你提供的信息一样准。22
2.6 免编译直接跑:strip types 与它的代价
大项目的 TypeScript 编译动辄几分钟,是流畅开发流程里的减速带。出路之一是根本不编译:
Node.js 22.6.0 起,node --experimental-strip-types hello.ts 可以直接跑大多数 TypeScript——
把类型注解像剥皮一样剥掉再执行。Deno 和 Bun 更早就有同样能力。23
但有一个大坑要当场记住:剥类型 ≠ 做检查。 直接运行时,类型检查是另一个独立步骤,
要自己跑(Deno 默认会帮你跑,Node 不会)。「能跑」和「类型安全」从此是两件事。
另外 enum、namespace 这类「不只是注解」的 TypeScript 特性剥不掉(它们是 TypeScript 少数会生成代码的特性,
原书整本不用);TypeScript 5.8 加了 erasableSyntaxOnly 选项,强制你的代码只含可剥除的语法。24
编译并没有死:浏览器短期内不会原生支持 TypeScript;压缩混淆时照样要剥类型; 要支持旧运行时照样要 transpile 到指定 ECMAScript 版本。而多数开发者真正依赖的是编辑器里的实时反馈: VS Code 里类型错误随写随标,悬停函数名就能看到它的完整类型签名。25
这一段作者的判断与证据要分开: 「大型项目全量编译要几分钟」是作者陈述的体验事实(无数据); 「编辑器实时反馈 + CI 偶尔全量检查」是作者推荐的工作流(属立场,非标准答案)。
3. ESLint:把「约定」变成规则,把「破例」变成注释
3.1 静态分析的第二支柱
测试能证明代码行为,但写测试贵;而且就算 100% 覆盖率(每行代码都被执行过), 没料到的条件组合照样可能出事。静态分析是另一路:不跑代码,直接检查代码文本里可能的错误。 TypeScript 管「类型的误用」,ESLint 管「其他一切」——它是可高度定制的规则引擎。26
装法和 TypeScript 一脉相承:npm install --save-dev eslint @eslint/js typescript typescript-eslint,
再写一份 eslint.config.mjs 配置(以 typescript-eslint 第 8 版为准):27
import eslint from "@eslint/js";
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";
export default defineConfig([
{ extends: [eslint.configs.recommended] }, // 所有文件:核心规则
{
files: ["**/*.{ts,mts}"], // 只对这些文件:
extends: [
tseslint.configs.strictTypeChecked, // 严格规则(要类型信息)
tseslint.configs.stylisticTypeChecked, // 风格规则(要类型信息)
],
rules: { "@typescript-eslint/no-non-null-assertion": "off" },
languageOptions: { /* 告诉 ESLint 去哪找类型检查器 */ },
},
]);配置里的关键区分:核心规则对 JS/TS 通用、无条件套用;带 TypeChecked 后缀的规则集 需要接入 TypeScript 的类型检查器——慢得多,但能覆盖的问题范围也大得多 (比如「这个 if 的条件永远为真」,不看类型根本判不出来)。28
3.2 主线旅程的 ESLint 段:一个「永远为真」的条件
书里给了一段故意有问题的代码:29
const ninja = Math.random() > 0.5 ? "ryu" : "naruto"; // 类型:"ryu" | "naruto"
if (ninja === "ryu" || "naruto") { /* 报错 no-constant-condition */ }
if (ninja) { /* 报错 @typescript-eslint/no-unnecessary-condition */ }两个 if 都永远为真,但被抓的方式不同:
- 第一个的错「不看类型也能抓」:
x === "ryu" || "naruto"里||右边是个非空字符串常量, 整个条件必真——核心规则no-constant-condition直接判出; - 第二个的错「看了类型才知道抓」:
ninja的类型是两个非空字符串的联合, 永远 truthy(转布尔为真)——只有no-unnecessary-condition这种吃了类型信息的规则才看得见。
规则名带 @typescript-eslint/ 前缀的,都来自插件而非 ESLint 核心——这是读任何 lint 输出都要会认的命名空间(「名字起在哪一片」,用来区分规则出处)。30
3.3 当规则碍事:三种应对,按顺序排
规则开多了,迟早有一条跟你作对。书里给的例子是个无 bug 的计数函数:
const dojoStudents = new Map([["Hidden Leaf", 2]]);
const enrollNinja = (dojoName: string) => {
if (!dojoStudents.has(dojoName)) {
dojoStudents.set(dojoName, 0);
}
dojoStudents.set(dojoName, dojoStudents.get(dojoName)! + 1); // 这里的 ! 被规则点名
};Map.get 查不到时返回 undefined,而这里上一行刚保证过键存在——于是用了 TypeScript 的
非空断言 !(「这值不是 undefined,我保证」)。严格规则集里的
no-non-null-assertion 偏偏禁它,理由是:断言是人的判断,人判断错了,类型检查这层防线就有洞。31
应对有三档,原书逐一给了语法:
- 改代码迎合规则——有时值得,有时为规则扭曲代码,不值;
- 项目级关掉规则——配置里
"@typescript-eslint/no-non-null-assertion": "off",一刀切,省事但放弃了这条规则的其它预警; - 按行豁免,并写下理由——书里推荐的折中:
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion --
* 上一行 has() 已保证这个键存在。
*/
dojoStudents.set(dojoName, dojoStudents.get(dojoName)! + 1);// eslint-disable-next-line [规则名] [-- 解释] 是「内联配置」:只豁免紧跟的一行,
解释部分留在代码里,日后读代码的人(和未来的你)都知道为什么此处破例。32
3.4 ESLint 的另一半:风格警察
ESLint 不只抓 bug,还能风格执法,而这是它暗合本书主题的地方。2008 年 Douglas Crockford 写了本 《JavaScript: The Good Parts》,主张只用这门语言的「好部分」、躲开「坏部分」; 此后 ECMAScript 每年加新特性,选择越多、认知负担越重,ESLint 规则就成了给复杂性配重的手段—— 团队一旦决定禁掉某个写法,用一条规则把它变成机器强制。33
书里点了两类例子:核心规则 no-shadow 禁遮蔽(内层声明与外层同名的变量,
读者容易搞混到底在读哪个);以及 Sindre Sorhus 的 eslint-plugin-unicorn 插件,
一百多条强烈个人风格的规则,从「创建 Error 必须带 message」到「禁用 null」。34
自定义规则也是可能的(原书预告后续章会讲怎么写自己的规则)——MEAP 里还没有,点到为止。
4. Prettier:格式问题的「终战方案」
没有人喜欢没完没了的代码行,也没有团队喜欢为「单引号还是双引号」开会。 ESLint 早年有行长规则,但「超了怎么办」的负担压在开发者身上;Prettier 的回答是: 自称「有主见的代码格式化工具」——选项故意少到没得吵,大多数团队视之为 blessing(福音)。35
装法:npm install --save-dev prettier,再 echo '{}' > .prettierrc。
空配置也是个重要动作:它阻断 Prettier 向上级目录找配置的企图,也让编辑器的 Prettier 插件认出
「本项目启用了 Prettier」。36 最常改的三个选项:singleQuote(单引号偏好,默认关)、
useTabs(默认关,关时用 2 空格缩进)、semi(默认每句补分号)。
然后跑 npx prettier --write .,见证一坨乱代码(随意的断行、Math.random () 里多余的空格、
单双引号混用、if/else 缩进不齐)被重排成统一格式的全过程。37 细节如分号、引号会按配置统一;
行宽没有硬限制,默认 printWidth 80 是「努力目标」,算法会合理断行而不是硬切。
一个顺手的小坑:prettier --write . 会顺手格式化它认识的所有文件类型——包括工具生成的
package-lock.json。不想让它碰,建一个 .prettierignore 把生成文件排除。38
4.1 让格式化自动发生:Husky 与 pre-commit
编辑器插件让「保存即格式化」在个人机器上成立,但你不能保证协作者也这么设。 Husky 利用 git 的 pre-commit 钩子(hook,钩子是 git 在特定动作前后自动执行的脚本), 让「提交前自动跑一遍工具」对整个仓库所有人生效。39
机制链条(原书逐步给了命令):装 husky → npx husky init → package.json 里出现
"prepare": "husky" → npm 在每次 npm install 后自动执行 prepare,把 git 的钩子目录指到
项目的 .husky/ → 在 .husky/pre-commit 里写 prettier . --write 和 git update-index --again
(让 git 把 Prettier 改过的内容算进本次提交)——完成。40
大项目里「每次提交格式化全仓库」不划算,lint-staged 可以让钩子只处理本次暂存(staged)的文件。41
作者的工程判断(当立场读): pre-commit 里塞得越多越慢,摩擦越大,贡献者越烦; 尤其 TypeScript 类型检查必须看整个项目、无法只看暂存文件,大项目通常不在 pre-commit 里跑 TypeScript 和 typescript-eslint,把这些挪到提交后的 CI(持续集成)流水线。42
5. 作者的判断与证据
本章是教程章,判断多、纯论证少,分开标:
书内可验的事实/机制:
- 类型检查器「只看声明处上下文」于是放弃推断、落入隐式 any——这是可由报错信息直接验证的机制。43
- 编译产物中类型全部消失、运行时行为与 JavaScript 一致——把代码编译一遍即可验证。44
- 微任务/宏任务的排队规则不在本章,但「promise 回调永不同步触发」的本章引用,机制见第 08 章。
作者立场(有真实依据,但是选择而非必然):
- 选 strict 全家桶 + TypeChecked 规则集——比默认更严,是「忍者」取向;
- 「规则破例要附解释注释」是作者提倡的纪律,不是 ESLint 的要求;
- 「类型检查别进 pre-commit,挪去 CI」是经验之谈,小项目完全可以反过来。
6. 边界与局限
- 版本敏感。 本章写于 typescript-eslint v8、Husky 新版、Node 22.6 的实验特性时期; 命令与配置格式随版本变动快,动手时以官方文档为准(书中自己也给了链接)。
- 配置细节不在书内。
tsconfig.json全书级的深入配置被原书明确跳过,指了外部 cheat sheet; 本书只负责「让你跑起来」。 - MEAP 缺口: 写自定义 ESLint 规则预告在后续章,本版没有;
enum/namespace因为和 strip-types 不兼容被整本回避,需要这些特性时本章模式不适用。 - console.assert 的约定: 原书示例代码的验证全靠
console.assert(condition, message)—— 条件为真无输出,为假打印消息。看懂它只需这一句,但它不是测试框架,别当成测试工程入门。
7. 可带走的(合上这一章,你应该有这九条)
- 两个轴分开记:动态/静态类型是「什么时候检查」,隐式转换是「会不会偷偷改类型」; TypeScript 把前者提前到编译期,管不了后者本身,但能预判后者的结果。
- 类型错误不阻塞编译:
tsc报完错照样吐.js;--noEmit用于只查不编。 - any 是检查关闭阀:隐式 any 在 strict 下报错;线索给足(注解、interface、默认值),检查器就能推断返回值。
- 类型在运行时不存在:
instanceof一个 interface 是错;类型收窄靠运行时检查(in、typeof、比较), 编译后类型全擦除。 - 收窄跨不过函数调用:需要时用
x is T谓词(注意它同时强制反方向推断)或as(慎用)。 - 剥类型 ≠ 类型检查:
node --experimental-strip-types直接跑 TS 时,检查要单独做。 - 带 TypeChecked 的规则比核心规则慢得多、强得多;规则碍事时按「改代码 → 项目关 → 按行豁免附理由」的顺序选。
- Prettier 的哲学是「少配置,没得吵」;
.prettierrc即使是空文件也有存在的意义(阻断向上找配置)。 - pre-commit 不是垃圾桶:塞得越多摩擦越大;类型检查这类全项目操作挪去 CI。
9. 深读一:tsconfig 的五个问题,与 strict 的连锁选项
第 2 节的 tsc --init 一带而过,这里把配置文件到底在回答什么问题讲透。
原书列了四个关键问题,我们加上「吐不吐文件」补成五个:41
1. 编译哪些 TypeScript 文件? —— include / files 字段
2. 从别处带哪些类型进来? —— types、typeRoots(第三方包的类型声明)
3. 检查多严格? —— strict 及它连锁打开的一排开关
4. 吐不吐编译产物?吐的话目标是哪代 ECMAScript? —— noEmit / target
5. (模块怎么组织?) —— module(后续章的主题)npx tsc --init 生成的 starter 配置,书里给了它的实际输出:
target: es2016 // 语法降级到 ES2016(兼容较老运行时)
module: commonjs // 模块标准(第 3 章的 .mjs 话题与之相关)
strict: true // 严格模式总开关
esModuleInterop: true // 让 ESM/CommonJS 互操作顺滑
skipLibCheck: true // 跳过库文件的类型检查(加速)
forceConsistentCasingInFileNames: true // 强制文件名大小写一致strict: true 最值得展开——它不是单个检查,而是一组开关的总闸,
打开它会连带点亮包括 noImplicitAny(禁隐式 any)、alwaysStrict(在产物里加 "use strict")
在内的一整排选项。原书点了一句「Enabling strict mode is a rare exception to the rule that
the TypeScript compiler doesn't add code」:strict 点亮了 alwaysStrict,产物里才会出现那句
"use strict"——这是编译器几乎不生成代码的原则的一个罕见例外。42
配置的门道原书明确说不深讲(指了官方 tsconfig 参考和 Matt Pocock 的速查表),
但它给了选型上的安心剂:tsc --init 的默认值对本书所有示例都够用——
起步用默认,遇到具体需求再查参考,是合理的路线。
10. 深读二:any 的全貌,与「检查器怎么推断」的四个样本
第 2 节讲了 any 是「检查关闭阀」,这里把推断机制的样本补全—— 同样是「不写类型」,检查器在四个不同的位置上给出的待遇完全不同:
let x = 5; // 样本①:初始化给了值
// 推断:number——检查器看赋值
addEventListener("keydown", (event) => { // 样本②:参数出现在已知签名里
console.log(event.key); // 推断:event 是 KeyboardEvent——从函数类型推出
});
const pluralize = (str, count) => /* ... */; // 样本③:箭头函数参数,无任何上下文
// 推断失败 → any → strict 下报错(noImplicitAny)
const performAction = (ninja: string, action = "skulking") => { /* ... */ };
// 样本④:参数有默认值
// 推断:action 是 string——从默认值反推四个样本的共同规则(书里的原话):检查器「doesn't look at any code outside of that initial declaration」——推断只看声明那一刻的上下文,不看别处。 样本③的「举手投降」不是检查器懒,是它在规则内真的没有办法。43
any 的使用纪律(书里给了分层意见):迁移老代码时,any 是有价值的逃生门——
先让文件编译过、再逐个角落收紧;新代码里,它应该被当作代码味道。
想连「显式写 any」也禁掉,有 typescript-eslint 的 no-explicit-any 规则兜底;
想在严格检查下声明「这个值的类型我说不好」,更妥的写法是 unknown(第 5 章的
类型谓词样例用过它)——any 是「别查我」,unknown 是「我也不知道,查的时候小心」。
11. 深读三:类型收窄的完整工具箱与两个天花板
第 2 节的收窄给了 in/typeof/Array.isArray/satisfies 的巡游,
这里把书里的 Listing 2.6 完整走一遍,它是「一次看全收窄手法」的标本:44
const getValueWithType = (value?: string | string[] | 23) => {
if (value === 23) { // 手法①:跟具体值比
return `number: ${value}`; // 这一支:value 收窄成字面量 23
}
if (typeof value === "string") { // 手法②:typeof 查原始类型
return `string: ${value}`; // 这一支:value 是 string
}
if (Array.isArray(value)) { // 手法③:Array.isArray 查数组
return `array: ${value.join(", ")}`; // 这一支:value 是 string[]
}
value satisfies undefined; // 手法④:走到这里,只剩 undefined 了
return "undefined";
};三个参数类型(string | string[] | 23,问号让它还能是 undefined)在三个 if 里被逐支摘掉,
最后剩下的唯一可能是 undefined——satisfies 在原地断言「此刻它就是这个类型」,
断言过则无事发生,不过则编译错。这个函数的最后一行之所以能放心返回 "undefined",
全靠前面三支把其他可能排空了——收窄的本质,就是在一行行代码里做排除法。45
两个天花板的完整版(书里各给了一段):
天花板一:可变结构。数组和对象被创建之后,它们的「内容类型」不会随写入而变窄:
const mixedArray = ["str", 123] 的类型是 (string | number)[],于是
mixedArray[0] 的类型是 string | number——哪怕你清楚第一个永远是 "str"。
对策 as const:字面量锁成只读元组 readonly ["str", 123],
mixedArray[0] 精确到 "str",代价是这个数组从此不可变(正好接上第 12 章的只读话题)。46
天花板二:跨函数不收窄。书里的对照实验值得整段记住:47
const stringOrNumber = ["str", 123][0]; // string | number
const isString = (x: unknown) => typeof x === "string";
const isShortString = (x: unknown) => typeof x === "string" && x.length <= 3;
if (isString(stringOrNumber)) {
stringOrNumber satisfies string; // ✓ 通过
}
if (isShortString(stringOrNumber)) {
stringOrNumber satisfies string; // ✗ 失败!
}isString 能收窄、isShortString 不能,差别在检查器的特判规则:
它只为「返回 true ⟹ 是 string;返回 false ⟹ 不是 string」这种双向都可推的函数做收窄。
isShortString 的 false 分支推不出「不是 string」(可能只是长了),
双向缺一,检查器一次推断都不做。解药 x is string 谓词的副作用前面讲过:
它强制双向成立,于是 isShortString 返回 false 时,检查器会推出参数是 never——
谓词是把双刃剑,签得越宽,else 分支越离谱。 as 是最诚实的兜底:
「关于这个变量,我知道一些你不知道的」——但检查器的准确度上限,就是你给它的信息。48
12. 深读四:ESLint 配置逐块解剖
第 3 节的 eslint.config.mjs 值得一块一块拆开(书里就是逐块讲的):49
import eslint from "@eslint/js";
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";
export default defineConfig([
{ // ── 块①:无条件适用所有文件
extends: [eslint.configs.recommended],
},
{ // ── 块②:只对此处 files 匹配的生效
files: ["**/*.{ts,mts}"],
extends: [
tseslint.configs.strictTypeChecked, // 严格规则(要吃类型信息)
tseslint.configs.stylisticTypeChecked, // 风格规则(要吃类型信息)
],
rules: { // 块内可覆盖单条规则
"@typescript-eslint/no-non-null-assertion": "off",
},
languageOptions: { // 告诉规则去哪拿类型信息
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
]);四件事一次说清:
files是作用域:块②的规则只扫.ts/.mts;配置文件自己(.mjs)只吃块①;TypeChecked后缀 = 要类型信息:strictTypeChecked 里的no-unnecessary-condition必须知道变量类型才能判「这条件永远为真」,所以需要languageOptions把 ESLint 接到项目的类型检查器上——代价是 lint 变慢,收益是规则变强;@ts-check注释:配置文件顶部那行// @ts-check让 JavaScript 文件也在编辑器里 获得类型反馈(跑tsc不会查它——它不在 tsconfig 的编译范围里, 反馈只活在编辑器中);- 规则名带命名空间:
@typescript-eslint/xxx来自插件,裸名来自核心—— 报错输出里认前缀,就知道该去哪查规则文档。
12.1 「永远为真」的两个样本,对照再看
块级规则装上后的首个猎物(书里 Listing 2.9):
const ninja = Math.random() > 0.5 ? "ryu" : "naruto"; // 类型:"ryu" | "naruto"
if (ninja === "ryu" || "naruto") { /* 错误 */ }
if (ninja) { /* 错误 */ }两行都报,但报错来源不同(书里的对照是这一节的关键教学):
- 第一行:核心规则
no-constant-condition抓的——||右边是个非空字符串常量, 整个条件必真,这跟 ninja 是什么类型无关,纯语法形态就露馅; - 第二行:插件规则
no-unnecessary-condition抓的——ninja的类型是两个非空字符串 的联合,天然 truthy,「如果(ninja)」恒真。这条必须类型信息在手才能看见。
跑 npx eslint 的输出两行报错并排躺着,命名空间各不同——这就是
「核心规则管形态、TypeChecked 规则管类型」的实证。50
12.2 风格规则的谱系:从 Crockford 到 unicorn
第 3 节末尾的风格话题,补全书里的历史线:2008 年 Crockford 的 《JavaScript: The Good Parts》主张「只用好部分、躲开坏部分」; 此后标准每年加特性,选择越多认知负担越重——ESLint 规则成了团队给复杂性配重的手段: 一旦决定禁掉某个写法,规则让决定不需要靠 code review 时的人肉记忆。51
书里点了两类例子,各自的出处:
- 核心
no-shadow:内层变量与外层同名(遮蔽),读者要随时记「此刻的 count 是哪个 count」。它不在 recommended 里(有人嫌起名麻烦),要开就得自己在 rules 里写"no-shadow": "error"; eslint-plugin-unicorn:Sindre Sorhus 把一百多条强烈个人意见铸成规则, 书里举了三个样本——创建 Error 必须带 message(约定)、文件名大小写统一(风格)、 禁用null(「好部分」的界定)。找不到合意的规则?写自己的(后续章)。
13. 深读五:Prettier 的行为边界,与「写进提交」的完整链条
13.1 格式化前后对照,逐类改动命名
书里的乱代码样板(Listing 2.12)被重排成 Listing 2.13,把每一类改动命名, 你以后看 Prettier 的 diff 就认得它在干什么:52
| 乱代码里的问题 | Prettier 的处理 |
|---|---|
const generateNinjaTurtleAction = 无意义断行 | 合并回单行(未超宽) |
Math.random () 括号前的多余空格 | 删空格 |
单双引号混用('Fight Shredder' / "Rescue April") | 统一成双引号(默认) |
| if/else 缩进层级不齐 | 统一缩进、补分号 |
} else { 的悬挂 | 花括号各归各位 |
配置侧再记三件小事:.prettierrc 即使是 {} 也有意义(阻断向上查找、
让编辑器插件认出项目);常改的三项 singleQuote/useTabs/semi 默认值是
false/false/true;行长没有硬上限——printWidth(默认 80)是「努力目标」,
算法优先选择合理的断行点,而不是到 80 列硬切。53
.prettierignore 的坑(书里实测踩过):prettier --write . 会格式化它认识的
所有文件类型——包括工具生成的 package-lock.json。生成物不该被格式化,
一行 package-lock.json 进 .prettierignore 即可。54
13.2 Husky 链条,逐文件走一遍
挂接 pre-commit 的完整链条涉及三个文件,书里逐个给了内容,这里串成一条流水线:55
package.json:
"scripts": { "prepare": "sh prepare.sh" } ← ① npm install 后自动跑
prepare.sh:
PROJECT_PATH=$PWD ← ② 记住自己所在的目录
cd "$(git rev-parse --show-toplevel)" ← ③ 跳到 git 仓库根
husky "$PROJECT_PATH/.husky" ← ④ 把仓库的钩子目录指到 .husky
.husky/pre-commit:
cd "$(dirname "$(readlink -f "$0")")/.." ← ⑤ 钩子运行时定位到项目根
prettier . --write ← ⑥ 格式化
git update-index --again ← ⑦ 把改动并回本次提交每一步为什么存在:① prepare 是 npm 的生命周期脚本,裸 npm install 之后自动执行,
所以它做「装完依赖后的初始化」正合适;②–④ 的曲折是因为示例项目位于 git 仓库的
子目录——Husky 默认要求 package.json 在仓库根,prepare.sh 用两行 shell
把钩子目录指到子目录里的 .husky;⑤ 的 readlink -f 处理符号链接,
保证无论从哪触发提交都能定位项目根;⑦ 最容易漏:Prettier 改完文件后,
那些文件「改动后未重新暂存」的状态要让 git 知道——git update-index --again
把格式化结果计入本次提交,否则钩子里的格式化就白做了。56
大项目的改良有两层:lint-staged 让钩子只处理本次暂存的文件(而不是全仓库); 更重的检查(TypeScript、typescript-eslint、完整测试)别进 pre-commit—— 类型检查必须看整个项目,minutes 级的延迟会把贡献者烦走,把它们挪到 CI, pre-commit 只留秒级的事。「钩子里塞多少东西」是一个真实的摩擦预算问题。57
14. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| setTimeout 写反参数的开场 | Chapter 2: Using modern JavaScript tools | text/05-ch02-chapter-2-using-modern-javascript-tools.txt:47(搜「setTimeout:」) |
| 动态类型 vs 隐式转换 | 同上 | :73(搜「TYPES OF TYPED LANGUAGES」) |
| tsc --init 与 tsconfig | 同上 | :106(搜「tsconfig.json」) |
| 报错仍编译的机制 | 同上 | :155(搜「proceeded to compile the code anyway」) |
| 隐式 any 与推断边界 | 同上 | :205(搜「implicitly has an 'any' type」) |
| interface/联合/可选 | 同上 | :260(搜「interface StarshipOptions」) |
| 类型收窄与鸭子类型 | 同上 | :305(搜「type narrowing」) · :350(搜「won't work」) |
| 谓词与 as | 同上 | :447(搜「type predicate」) · :470(搜「escape hatch」) |
| strip types | 同上 | :481(搜「experimental-strip-types」) |
| ESLint 配置与规则集 | 同上 | :540(搜「Setting up ESLint」) · :613(搜「no-constant-condition」) |
| 规则豁免三档 | 同上 | :646(搜「Disabling ESLint rules」) · :723(搜「inline configuration」) |
| 风格规则与 Good Parts | 同上 | :746(搜「distinctive code style」) |
| Prettier 与配置项 | 同上 | :806(搜「opinionated code formatter」) · :842(搜「singleQuote」) |
| Husky 与 pre-commit | 同上 | :910(搜「Husky」) · :971(搜「pre-commit script」) |
| lint-staged 与 CI 建议 | 同上 | :985(搜「lint-staged」) · :993(搜「tradeoff」) |
| 全章总结 | 同上 | :1001(搜「Summary」) |
Footnotes
-
出处:「Chapter 2: Using modern JavaScript tools」第 45–59 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:54,搜「something strange happens: nothing」)。浏览器里 setTimeout 参数写反是静默失败,Node 才报错——同一 bug 两种死法。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 60–70 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:64,搜「superset of the JavaScript language」)。GWT 的替代路线与 TypeScript 的超集路线对比。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 73–90 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:73,搜「TYPES OF TYPED LANGUAGES」)。原书把「没类型」的抱怨拆成动态类型与隐式转换两件事。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 83–89 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:84,搜「results in the string」)。隐式转换的例子与「类型检查器能预判转换结果」的机制。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 91–104 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:103,搜「save-dev typescript」)。装包与 --save-dev 的含义(devDependencies,第 3 章展开)。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 106–130 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:120,搜「npx tsc --init」)。init 生成配置列出的五个默认开关。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 138–156 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:149,搜「TS2345」)。报错原文与「精确位置」体验。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 155–163 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:158,搜「proceeded to compile the code anyway」)。「报了错照样编译」是原书特意点出的新手坑。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 169–179 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:170,搜「strip out type annotations」)。编译器只做减法、不加代码的原则,与「零成本」论断。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 182–212 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:205,搜「implicitly has an 'any' type」)。pluralize 例子与 TS7006 报错原文。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 214–226 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:220,搜「throws up its arms and gives up」)。检查器放弃推断、退到 any 的原文比喻;any「effectively disables type checking」同段。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 231–243 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:239,搜「annotations also give the type checker enough information」)。注解加线索、返回值被推断的机制。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 258–281 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:260,搜「interface StarshipOptions」)。interface、联合|、可选?三样小机制同段出现。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 283–296 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:286,搜「Listing 2.4, or with the type keyword. The」)。引自 TypeScript 官方手册的 interface vs type 建议。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 305–344 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:334,搜「type narrowing」)。Mage|Thief 例子与 TS2339 报错。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 341–344 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:342,搜「can't be a Thief」)。收窄的推理:「检查过有 magic 属性,所以不可能是 Thief」。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 345–357 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:345,搜「duck typing」)。鸭子类型的来历与 instanceof 报错原文。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 359–370 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:359,搜「The compiled JavaScript code from this example contains」)。编译产物中类型消失、只剩运行时检查。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 372–398 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:376,搜「assortment of type narrowing」)。=== 具体值、typeof、Array.isArray、satisfies 四种收窄手法的巡游。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 399–445 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:401,搜「mutable structures」)。两个天花板:可变结构跟踪失败、跨函数不收窄;isShortString 的反例同段。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 446–468 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:450,搜「x is string」)。类型谓词语法与「else 分支被推断成 never」的副作用。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 468–479 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:470,搜「the as keyword. When you use as, you’re」)。as 的语义(「我比你知道得多」)与「only as accurate as the type information you provide」的警告。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 481–500 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:485,搜「Thankfully, it’s become possible to run TypeScript directly」)。Node 22.6 直接跑 TS 的命令与 enum/namespace 例外。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 501–509 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:501,搜「Running TypeScript code directly has one big caveat」)。剥类型≠类型检查的坑与 erasableSyntaxOnly 选项。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 510–519 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:510,搜「real-time feedback」)。编辑器实时反馈 + CI 全量检查的作者工作流。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 523–538 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:535,搜「analysis tool for everything else」)。ESLint 定位:TypeScript 管类型误用,ESLint 管其余一切。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 540–558 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:553,搜「npm install --save-dev eslint」)。安装命令与四包清单(eslint、@eslint/js、typescript、typescript-eslint)。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 600–608 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:605,搜「TypeChecked suffix」)。TypeChecked 规则集需要类型信息、慢但覆盖广的取舍。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 609–641 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:613,搜「const ninja = Math.random() > 0.5 ? "ryu」)。ninja 常量与两个永远为真的 if。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 636–641 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:638,搜「from typescript-eslint’s strict rule set. (All rules that」)。带命名空间的规则来自插件的辨认方法。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 656–697 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:691,搜「Forbidden non-null assertion」)。enrollNinja 例子与 no-non-null-assertion 的禁由。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 698–740 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:723,搜「ESLint allows you to make exceptions to rules」)。三档应对与按行豁免的注释格式。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 746–760 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:747,搜「The Good Parts」)。Crockford 的书与「规则给复杂性配重」的论点。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 760–804 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:794,搜「eslint-plugin-unicorn」)。no-shadow 与 unicorn 插件的例子清单。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 806–819 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:813,搜「Those dark days are long gone thanks to」)。Prettier 自我定位与「Just use Prettier」的行业默契。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 821–840 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:835,搜「.prettierrc」)。空配置文件的两个作用。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 856–895 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:876,搜「prettier --write」)。乱代码被重排的完整演示。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 897–905 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:900,搜「.prettierignore」)。package-lock.json 被顺手格式化的小坑。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 910–921 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:916,搜「pre-commit hook」)。Husky 利用 git 钩子自动跑脚本的机制。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 922–983 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:934,搜「lifecycle scripts」)。prepare 生命周期脚本与 pre-commit 脚本内容。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 984–992 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:985,搜「lint-staged」)。只处理暂存文件的优化。 ↩ ↩2 -
出处:「Chapter 2: Using modern JavaScript tools」第 993–999 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:993,搜「tradeoff」)。pre-commit 越慢摩擦越大、类型检查挪 CI 的作者建议。 ↩ ↩2 -
出处:「Chapter 2: Using modern JavaScript tools」第 214–220 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:218,搜「It doesn't look at any code outside」)。「只看声明处上下文」的机制边界,可直接由报错验证。 ↩ ↩2 -
出处:「Chapter 2: Using modern JavaScript tools」第 352–358 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:352,搜「The problem with this code is that it」)。「类型在运行时不存在」的原话与编译产物对照。 ↩ ↩2 -
出处:「Chapter 2: Using modern JavaScript tools」第 376–382 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:392,搜「Checking for equality with a primitive value」)。各收窄支路的注释说明。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 399–413 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:401,搜「mutable structures」)。可变结构天花板与 as const 对策。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 414–445 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:418,搜「stringOrNumber」)。isString/isShortString 对照实验的完整代码。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 446–479 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:450,搜「x is string」)。谓词的双向强制与 never 副作用;as 的「I know something」语义见第 470 段。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 559–608 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:569,搜「export default defineConfig」)。配置文件两块的逐块说明与 files 作用域。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 609–644 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:636,搜「no-constant-condition」)。两条报错来源的对照说明(「regardless of what ninja is」见第 639–641 段)。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 746–804 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:755,搜「counterweight」)。规则给复杂性配重的论点;Crockford 背景见第 747–749 段。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 856–895 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:859,搜「Listing 2.12」)。乱代码样板与格式化结果的对照(Listing 2.13 见第 883 段)。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 833–855 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:842,搜「singleQuote」)。三个常改选项的默认值;printWidth 的「目标而非硬限」语义见第 853–855 段。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 897–905 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:900,搜「.prettierignore」)。package-lock.json 被顺手格式化的问题。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 910–983 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:925,搜「npx husky init」)。Husky 装接的完整链条与 prepare.sh 的存在理由(「won't work if your package.json isn't at the root」见第 940–943 段)。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 945–983 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:952,搜「prepare.sh」);与第 973–979 段(搜「pre-commit script」)。prepare.sh 逐行注释(#B–#D)与 pre-commit 脚本内容(#A–#C)。 ↩ -
出处:「Chapter 2: Using modern JavaScript tools」第 984–999 段(
text/05-ch02-chapter-2-using-modern-javascript-tools.txt:985,搜「lint-staged」);与第 993–999 段(搜「tradeoff」)。lint-staged 的作用与「类型检查挪 CI」的摩擦预算论证。 ↩