跳转至

配置 TypeScript — tsconfig 基线、strict 与项目引用

这一章讲一件事: 把一个 TypeScript 项目配置对。 方法不是背选项手册,而是先抄一份九开关的基线,再按四个问题追加 (谁转译?跑在 DOM 吗?是库吗?在 monorepo 里吗?)。 读完你会拿到一份能直接用的 tsconfig,并理解里面每个开关在替你挡什么。

先正一次名:下面反复出现的「开关」是比喻,正式名字叫编译选项 (compiler option)——tsconfig 里 compilerOptions 下的每一项都是一条编译选项, 值多半是真布尔值(true/false),所以行话里也叫 flag(旗标)。本章行文沿用「开关」, 但去翻官方文档或别人的讨论时要认得 compiler option 这个词。

1. 顶层全景:一份配置 = 一组问题的答案

  基线九项(所有项目共用):
    skipLibCheck / target / esModuleInterop / allowJs / resolveJsonModule
    / moduleDetection: force / isolatedModules / strict / noUncheckedIndexedAccess

  追加四问:
    ① 用 tsc 转译吗?     是 → module: NodeNext(+ outDir + sourceMap)
                          否 → module: Preserve + noEmit(打包器转译)
    ② 跑在 DOM 吗?       是 → lib: ["es2022", "dom", "dom.iterable"]
                          否 → lib: ["es2022"]
    ③ 是库吗?            是 → declaration: true
    ④ 在 monorepo 里吗?   是 → 再加 composite: true + declarationMap: true

  图说:先背基线,再答四问——配置的复杂度被压到一张卡片上。

主线例子就是那九项基线本身:每一项都从「它挡下的事故」讲起。

2. 基线九项,逐项走一遍

{
  "compilerOptions": {
    "skipLibCheck": true,
    "target": "es2022",
    "esModuleInterop": true,
    "allowJs": true,
    "resolveJsonModule": true,
    "moduleDetection": "force",
    "isolatedModules": true,
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}
编译选项 干什么 它挡下的事故
skipLibCheck 不检查声明文件 node_modules 的类型错误淹没你的编译(第 14 章详解过)
target: es2022 编译产物对齐 ES2022 产物用了目标环境没有的语法
esModuleInterop CommonJS 与 ESM 互通 默认导入某些 CJS 包时翻车
allowJs 允许导入无影子的 .js 渐进迁移 JS→TS 时被自己的 JS 卡住
resolveJsonModule 允许 import JSON import config from "./config.json" 报错
moduleDetection: force 所有 .ts 按模块处理 隐式全局、name 撞车(第 14 章)
isolatedModules 禁用单文件转译器转不动的特性 换 esbuild/swc 时 silently 坏掉
strict 一组严格检查的总开关 null/undefined 泄漏、隐式 any 泛滥
noUncheckedIndexedAccess 索引访问结果为 T | undefined tracks[3] 运行时崩溃、编译期无感

前六项在之前的章节已经讲过机制,这里补一遍各自的「不开会怎样」:

  • skipLibCheck 不开:node_modules 里任何一个 .d.ts 的类型错误 (版本冲突、@types 之间的打架)都会以你的文件为坐标报出来, 编译输出被噪声淹没(第 14 章);
  • target 不设:默认是 ES5——产物多一层无谓的语法降级, 你可能以为自己在写现代 JS,其实在生产 ES3 风格的代码;
  • esModuleInterop 不开:从某些 CommonJS 包默认导入时得到 与预期不符的 undefined,而且错误发生在运行时;
  • allowJs 不开:项目里任何一个 .js 文件都无法被 import, 渐进迁移(JS 项目逐步转 TS)的第一步就卡住;
  • resolveJsonModule 不开:import config from "./config.json" 直接报错,只得改用手写类型 + fetch 之类的绕路;
  • moduleDetection 不开(默认 auto):隐式全局与 name 撞车 随时出现(第 14 章的经典事故)。

每一项都是「曾经有人被咬过,所以进了基线」——这不是风格清单, 是事故清单的反面。本章的力气花在最后两项上—— 它们决定了你的 TypeScript 是「严格的朋友」还是「客气的摆设」。

target:转译语法,不补 API

target 决定「新语法翻译成什么年代」。设成 es5 时,可选链和空值合并 这些新语法会被翻译成 ES5 写法:

// 你写的
const search = input?.search;
const defaulted = search ?? "Hello";

// target: es5 的产物
var search = input === null || input === void 0 ? void 0 : input.search;
var defaulted = search !== null && search !== void 0 ? search : "Hello";

但要记住它的能力边界:target 转译语法,不补 API。 "Hello".replaceAll(...) 在 target es5 下照样出现在产物里—— replaceAll 是 ES2021 的 API,不存在「翻译成旧写法」的说法; 你的目标环境没有它,运行时就炸,TypeScript 不会替你 polyfill1。 「执行环境认识哪些 API」由 lib 选项管(第 14 章)——target 管语法, lib 管 API,两件事别混2。拿不准时,让 target 跟上 lib 的版本3。

esModuleInterop:一次 2018 年的对齐

这是个老开关(2018 年):当年 TypeScript 处理 CommonJS 的默认导出和 通配导入时,与 Babel 等主流工具行为不一致;esModuleInterop 把 TypeScript 拉齐到了社区行为。应用项目永远打开,原书只给了结论, 细节链 release notes4。你只需知道:关掉它,CJS/ESM 混用的项目 会在某些 import 写法上莫名翻车——没必要省这一个布尔值。

isolatedModules:为「换转译器」买保险

解决什么问题: 越来越多项目不用 tsc 转译,改用 esbuild、swc、Babel 这些单文件转译器——快,但有个先天限制:它们只看一个文件, 没有全项目的上下文。有些 TypeScript 特性离开全项目上下文就转不对, isolatedModules 的作用就是禁用这些特性,让代码对单文件转译器安全**5。

典型受害者:ambient 的 const enum。

declare const enum AlbumFormat {
  CD,
  Vinyl,
  Digital,
}

const largestPhysicalSize = AlbumFormat.Vinyl;

const enum 的成员值在编译期被内联——这需要「知道整个项目里这个枚举 长什么样」的全局信息。tsc 有;esbuild 没有。isolatedModules 打开时, 这段代码直接报错:Cannot access ambient const enums when 'isolatedModules' is enabled——产物里会留着一个运行时根本不存在的 AlbumFormat 引用6。

原书对 isolatedModules 的定性是「明智的默认」:它禁用的模式极少, 却让你的代码随时可以换转译器——用一点点限制换很大的可移植性7。 (还记得第 10 章吗?const enum 本来就该少用;isolatedModules 等于把这个 建议变成了编译器强制。)

这个开关的风险形态值得单独说清:它防的是「换工具链时的静默损坏」。 没有它,你的代码用 tsc 编译一切正常;某天构建团队为了提速换成 esbuild, 构建依然「成功」,产物却悄悄变了——ambient const enum 的引用没有内联, 运行到那一行才炸,而构建系统一无所知。这类「转译器差异」事故在多工具链 项目里反复发生,isolatedModules 把检查前置到了写代码的那一刻: 错误出现在它最便宜的地方6。这个「前置到最便宜处」的模式, 和 noUncheckedIndexedAccess(把运行时崩溃前置成编译期红线)、 strict(把 any 泛滥前置成参数处的报错)是同一个工程哲学—— 本章所有开关的受益者,都是「未来的你」。

3. strict:总开关,以及为什么「关掉它」不是选项

strict 不是单个检查,而是一组严格选项的总开关:开了它, strictNullChecks(null/undefined 不能乱赋值)、noImplicitAny (参数不许裸奔成 any)等一起生效8。

看两个 strict 成员的「关掉后」形态:

// strictNullChecks: false 的世界
let name: string = null; // ✅ 不报错——null 塞进了 string

// noImplicitAny: false 的世界
function greet(name) { /* name 是隐式 any,什么都放行 */ }

'第一个例子是全书反复讲的「null 缺口」直接敞开;第二个是 any 泛滥的 水龙头。两个都是 strict 打开时你在第 05、06 章已经习以为常的红线9。

值得知道的是,strict 的内部是可拆解的:它是若干子开关的总和, strict: true 等于一次全开。子开关里与你日常最相关的两个:

  • strictNullChecks:null 和 undefined 不再能随意赋给任何类型—— 第 06 章整个「null 处理」话题的存在前提就是它;
  • noImplicitAny:任何地方类型推断不出时,报错而不是静默 any—— 第 04 章「参数必须注解」的强制版。

关掉 strict 但保留某个子开关的组合技术上可行,但社区库按全 strict 测试, 组合状态等于自定义方言——非必要不要散装。

原书对「strict 关掉当新手坡道」这个流行论点的反驳,值得完整转述, 而且还有一个全书层面的背景要一起摆上桌面:这本书从头到尾都假定 strict 开着—— 这不是本章的偏好,是全书的写作前提43。

  1. 主流库假定 strict 开着:zod、tRPC、Redux Toolkit、XState 在 strict 关闭时行为不符预期——类型推断结果、某些 API 的可用性都会变10;
  2. 社区资源默认 strict:Stack Overflow、React TypeScript Cheatsheet 的答案都按 strict 语境写,关掉后「照抄答案却对不上」会成为常态;
  3. strict: false 的项目大概率永远 false:成熟代码库再开 strict, 报错可能数以千计,没人敢修。

把第 3 条展开,它是一个真实的财务问题:一个五年历史、strict 关闭的 代码库,某天决定开 strict——tsc 报出 4,000 个错。修完它们需要 三人三周,期间主线还在前进,冲突不断;于是改成「新文件 strict、 旧文件慢慢迁」,而 TS 没有按文件开 strict 的开关(strict 是项目级的), 迁移方案只能用 // @ts-nocheck 逐文件压错(第 12 章说过别用它)—— 烂账从开关一路滚到注释。这就是为什么「先开再说」比「以后开」便宜 几个数量级:同样的 strict,在项目第一天是零成本,在第五年是组织工程。

原书给的定性很重:「把 strict: false 看作 TypeScript 的一个分叉版本—— 大量库用不了、求助更难、运行时错误更多。」11 整本书都建立在 strict 开启的前提上;它是现代 TypeScript 的地基。

4. noUncheckedIndexedAccess:用编译期的啰嗦换运行期的安全

解决什么问题: 索引访问在运行时可能越界,而默认的 TypeScript 假设 它永远成功。把默认行为的心智模型说透:默认的 TypeScript 认为 arr[i] 的结果是「这个数组元素类型」——前提是「你不会越界」, 而「你不会越界」编译器无从验证。这是一个善意的假设: 它让日常代码保持清爽(不用每行都处理 undefined), 代价是把一类运行时崩溃(越界访问)推出了类型系统的边界。

interface VinylSingle {
  title: string;
  artist: string;
  tracks: string[];
}

const egoMirror: VinylSingle = {
  title: "Ego / Mirror",
  artist: "Burial / Four Tet / Thom Yorke",
  tracks: ["Ego", "Mirror"],
};

const bSide = egoMirror.tracks[1];
console.log(bSide.toUpperCase()); // 'MIRROR' ✅

const nonExistent = egoMirror.tracks[3];
console.log(nonExistent.toUpperCase()); // VS Code 无感;运行时炸:
                                        // TypeError: Cannot read property of undefined

tracks 只有两首,索引 3 是 undefined——TypeScript 默认认为索引结果 永远有效,所以编译期安静、运行时崩12。

noUncheckedIndexedAccess: true 把规则改成:索引访问的类型是 T | undefined,而不是 T:

const ego = egoMirror.tracks[0];
// 悬停 → const ego: string | undefined

console.log(ego.toUpperCase()); // ❌ 'ego' is possibly 'undefined'

把这个开关的开与关并排成一张对照表,选择就清晰了:

  tracks[1] 的悬停类型:
    关 → string          「相信我,你不会越界」
    开 → string | undefined 「我说的是真话:运行时它可能是 undefined」

  tracks[3].toUpperCase() 编译期:
    关 → 安静              运行时 TypeError
    开 → 红线              迫使你表态后再调用

  每个访问点的代码量:
    关 → 0                开 → 一次表态(! / ?. / if)

选择 essentially 是「把崩溃风险集中到最后一个测试阶段发现(关)」 还是「摊到每个访问点当场发现(开)」。

注意一个「副作用」:连合法的索引也变得要小心——ego 明明是存在的 第一首,类型上也要先处理 undefined 才能调方法13。 这正是这个开关的代价:数组和索引签名的每一次访问都要对空值表态, 代码会变啰嗦。

啰嗦的形态有谱系,从轻到重:

// 形态一:非空断言(知道它存在时,第 12 章的 !)
console.log(ego!.toUpperCase());

// 形态二:可选链(不在乎它可能不存在时)
console.log(ego?.toUpperCase());

// 形态三:显式收窄(真的有逻辑支撑时)
if (ego) console.log(ego.toUpperCase());

// 形态四:解构带默认值(有合理兜底时)
const [first = "Unknown"] = egoMirror.tracks;

四种都是合法的表态方式,区别只在「你的信心来自哪里」。 noUncheckedIndexedAccess 没有强迫你用哪一种——它只强迫你选一种。 「必须表态」是这个开关的全部要求,也是它全部的价值: 表态留下的痕迹(一个 ! 或一个 if),就是未来读者理解你信心的坐标。

原书的态度是诚实的:「它让 TypeScript 更严格,代价是更仔细地处理 undefined——通常是划算的,因为它在开发早期就抓住潜在运行时错误。 但如果你在某些项目里把它关了,我们也不怪你。」14 '实操上的折中:新开的项目默认开(从一开始就习惯表态),存量项目按模块 逐步迁移;配合第 06 章的收窄工具,啰嗦程度可以接受。

一个让存量迁移更具体的路线:先只在「边界层」开——数据进入应用的 入口(解析器、API 客户端、存储读取)是最常见越界来源,把入口模块的 索引访问逐个点上表态,等于给整个应用装上防崩闸门;内部纯逻辑 (循环索引、固定结构)可以在开关全局打开后逐步跟进。迁移的报错误 别当敌人删(用 ! 压过去),当路标读:每个报错都在问「这个索引, 你凭什么信任它?」——答不上来的,就是真 bug。

判断(我们的,不是书里的): noUncheckedIndexedAccess 是「类型系统的 诚实度」测试:开了它,类型说的「string」就是运行时真的 string; 不开,「string」其实是「string,只要你没写错索引」。前一种类型系统 值得信任,后一种在重构时会漏。我们同意原书「默认开」的立场, 并且建议把它和 strict 同等对待——它是 strict 精神的补完: strict 管赋值诚实,它管访问诚实。 如果错,会错在: 对以索引访问为绝对热点且已有多层防御的代码库 (比如每一层都有长度检查的解析器),它带来的噪音可能超过收益; 此时关掉它是理性选择——但要清醒地知道,你放弃的是「索引结果的类型 就是运行时类型」这条保证。

5. 更进一步的严格选项(清单)

strict + noUncheckedIndexedAccess 之后,还想加码可以按表取用15:

选项 干什么
allowUnreachableCode return 之后的死代码报错
exactOptionalPropertyTypes 可选属性的类型必须「恰好」,不自动含 undefined
noFallthroughCasesInSwitch switch 的 case 不空穿(必须以 break/return/throw 结束)
noImplicitOverride 重写父类方法必须写 override(第 09 章)
noImplicitReturns 函数每条代码路径都必须返回值
noPropertyAccessFromIndexSignature 有索引签名的对象只能用方括号访问
noUnusedLocals 声明了没用的局部变量报错
noUnusedParameters 声明了没用的参数报错

逐项补一句「什么时候值得开」:

  • noFallthroughCasesInSwitch:多分支状态机的克星开关。漏写 break 的 case 会静默贯穿到下一个分支——运行时行为错得无声无息; 开了它,每个非空 case 都必须以 break/return/throw 收场。 配合第 06 章判别联合的 switch 使用,几乎零成本。
  • exactOptionalPropertyTypes:对「可选属性」语义较真: {foo?: string} 不允许被显式赋 undefined。开它对 API 层友好 (区分「没传」和「传了 undefined」),但对依赖「可选 = 可能 undefined」 习惯的代码库噪音大——成熟项目再考虑。
  • noImplicitReturns:函数里 if (x) return 1; 后忘了兜底返回, 返回值变成 undefined——开了它,每条路径都要有返回值。
  • noImplicitOverride:第 09 章的 override 挂牌变成强制; 继承层级深的项目(组件框架、插件体系)收益最大。
  • noPropertyAccessFromIndexSignature:索引签名对象(第 07 章的 Record)只能用 obj["key"] 不能用 obj.key——强制「动态键用动态语法」, 可读性收益大于检查收益,看口味。
  • allowUnreachableCode:dead code 报错;多数团队交给 lint。

后两个(noUnusedLocals/Parameters)争议最大:它们本质上把 lint 的活 抢给了编译器,而 lint 工具(ESLint)能配置得更细(如允许 _ 前缀的参数)。 建议:了解即可,是否启用看团队对 lint 栈的选择。

6. module 二选一:NodeNext 还是 Preserve

module 决定 TypeScript 如何对待 import/export。二选一,问题只有一个: 谁负责转译?

NodeNext:用 tsc 转译时

用 TypeScript 编译器自己产出可运行的 JS(Node 项目、后端服务), 选 module: "NodeNext"(它同时隐含 moduleResolution: "NodeNext")。 TypeScript 在此模式下模拟 Node 的模块解析,支持 package.json 的 exports 字段;产物不经任何处理就能在 Node 里跑16。

NodeNext 模式下,TypeScript 扮演的是「Node 的忠实秘书」: package.json 的 exports 字段、目录导入规则、条件导出—— 凡是 Node 的解析行为,它都模拟。好处是产物的行为在编译期就能被验证: 你写 import {x} from "pkg/sub" 时,TypeScript 会按 Node 的规则 解析这条路径,解析不到当场报错,而不是等部署后 Node 才炸16。

一个会绊人的细节:导入自己的 TypeScript 文件时,要写 .js 扩展名:

// 导入 album.ts 时:
import {Album} from "./album.js";

看着别扭,理由很正:TypeScript 不改写你的 import 语句——产物里的路径 就是 ./album.js;「你写的代码」和「你将运行的代码」保持一致, 是 TypeScript 的偏好17。Node16 是「钉死某个 Node 版本行为」的替代值, 但 Node 16 已 EOL,后续版本照搬了它的解析;原书建议要么用 NodeNext, 要么盯紧文档18。

Preserve:打包器转译时

用 Webpack、Rollup、Parcel、Vite,或任何前端框架(Next.js、Remix、 SvelteKit)时,选 module: "Preserve"——模块解析交给打包器, TypeScript 原样保留你的 import 语法。所以不用写 .js 扩展名:

import {Album} from "./album"; // ✅ 打包器负责解析路径

Preserve 的好搭档是 noEmit: true——两者说的是同一件事: 转译不归我管19。noEmit 阻止 tsc 产出 JS 文件 (默认它是会产的,发现跑 tsc 多出一堆 .js 时就是它没关)20。

NodeNext 与 Preserve 的抉择,可以压缩成对打包器的三个问题:

  我的代码离开 tsc 之后,还要经过别的工具吗?
    ├─ 不用(Node 直接跑产物)→ NodeNext
    │   附加义务:import 写 .js 扩展名;产物自己负责运行
    ├─ 用(Vite/webpack/框架)→ Preserve + noEmit
    │   附加义务:类型导入写 import type(isolatedModules 配套)
    └─ 两边都用(库既要发 npm 又被打包器消费)→
        看主战场:库以 NodeNext 为准,应用侧用 Preserve;
        monorepo 里这通常意味着「包一个配置、应用各一个配置」

2026 年的现实是:应用端几乎全在 Preserve 阵营(框架代管), NodeNext 主要服务于库、后端服务和工具脚本。选错阵营的典型症状: Preserve 项目里写 .js 扩展名(多余)或 NodeNext 项目里不写 (运行时找不到模块)——配置审计时这个开关是最常见的错误项1719。

sourceMap:让报错指回 TypeScript

tsc 转译的项目加 sourceMap: true,产物附带 .map 文件; Node 侧用 node --enable-source-maps dist/index.js 运行, 运行时报错的堆栈就指回 .ts 源码行21。代价只是多几个文件, 收益是线上错误的可读性——默认建议开。

7. 为库开发:declaration 与 monorepo 的跳转体验

outDir 与 declaration

写库时,outDir: "dist"(+ 可选 rootDir: "src")指定产物目录: src/index.ts → dist/index.js22。

关键开关是 declaration: true——tsc 在产 JS 的同时自动产出 .d.ts:

// 你写的 album.ts
export interface Album { title: string; artist: string; year: number; }
export function createAlbum(title: string, artist: string, year: number): Album {
  return {title, artist, year};
}
// 产出的 album.d.ts —— 只有类型,没有实现
export interface Album { title: string; artist: string; year: number; }
export declare function createAlbum(
  title: string, artist: string, year: number,
): Album;
// 产出的 album.js —— 只有实现
function createAlbum(title, artist, year) { return {title, artist, year}; }

'两张皮,各司其职——库的使用者拿到的正是「类型影子 + 运行实现」 这对组合(第 14 章的消费者视角,在这里是生产者视角)23。

把产物目录的真实形态看一眼:

  dist/
    album.js      ← 运行实现
    album.d.ts    ← 类型影子(declare function,无函数体)
    album.js.map  ← sourceMap(若开)
    album.d.ts.map ← declarationMap(若开)

四种文件,两对映射(js↔ts 源码、d.ts↔ts 源码),库的消费者 与 monorepo 的协作者各自消费其中一层。package.json 里指好 "types" 字段,你的库就加入了类型供给链。

composite 与 declarationMap:monorepo 里的「跳到源码」

monorepo 的场景:apps/my-app 引用 packages/my-library,两个包各有一个 package.json,边开发库边开发应用24。烦恼随之而来:在应用里 Command-click 一个来自 my-library 的 import,跳进的不是源码, 而是 my-library 的 .d.ts 影子——你在读影子,改不到真身。

declarationMap: true(+ sourceMap)解决这个:产出 .d.ts.map 文件, 在影子与源码之间建立映射;从此点击 import 直达 my-library 的 .ts 原文件25。 composite: true 则是项目引用的入场券,下一节讲。这两个开关加 declaration: true,是「库 + monorepo」的完整三件套; 库要发到 npm 时,declarationMap 价值下降(使用者不随包带源码), 但在 monorepo 里是生活质量的大幅提升26。

为什么这个「点击直达源码」值得专门一个开关?量化一下体验差: 没有 declarationMap 时,跨包跳转一天发生几十次(每次跳进 .d.ts 影子, 读完形状、猜完实现、再手动开源码文件,一次 30 秒到 2 分钟); 有了它,每次直达源码——按每天 30 次、每次省 1 分钟算, 一个五人团队一周省下的时间超过一个工作日。** declarationMap 是 典型的「生活质量开关」:不影响正确性,但持续征收「没开」的税, 而税率高得惊人。** monorepo 项目里,把它和 sourceMap 一起开, 应该是不需要讨论的默认项。

jsx:三个常用值

.tsx 文件里的 JSX 语法由 jsx 选项决定去向,五个值里常用的三个27:

值 干什么 什么时候用
preserve 原样保留 JSX 把转换留给下游(如 Vite/Next)
react 转成 React.createElement React 16 及更早
react-jsx 转成 _jsx 并自动引入 react/jsx-runtime React 17 及以后(现代默认)

2026 年的 React 项目基本就是 react-jsx。它的意义超出「语法转换」: react-jsx 模式下你不需要在每个文件顶部 import React—— jsx-runtime 的引入由编译器代劳,这也是 React 17 那个「新 JSX 转换」 改革的全部内容:JSX 不再是 React 的私有语法,而是可以被任何 jsx-runtime 实现消费的通用格式27。

8. Node 原生跑 TypeScript:未来的预演

Node 23+ 可以直接 node index.ts 跑 TypeScript 文件 (20–22 要加 --experimental-strip-types 标志)——不做编译, 只做「类型擦除」:把类型标注剥掉,直接执行28。

这个能力对代码有严格要求,对应 tsconfig 的五开关组合:

{
  "compilerOptions": {
    "module": "NodeNext",
    "noEmit": true,
    "rewriteRelativeImportExtensions": true,
    "verbatimModuleSyntax": true,
    "erasableSyntaxOnly": true
  }
}

逐一看:

  • module: NodeNext——用 Node 的模块系统(第 6 节)29;
  • noEmit——根本不产文件,直接跑 .ts(第 6 节)30;
  • rewriteRelativeImportExtensions——允许(并且需要)用 .ts 扩展名 导入:import {example} from "./example.ts"——因为没有 JS 产物, Node 必须直接指向 .ts 文件;这个开关让 TypeScript 接受这种写法31;
  • verbatimModuleSyntax——强制 import type 语法: import type {MyExampleType} from "./example.ts"。为什么?类型擦除器 分不清导入的是类型还是值;类型导入若不标 type,擦除器不敢删, 运行时就会去找一个不存在的模块而报错32;
  • erasableSyntaxOnly——第 10 章那个开关再次登场:只允许「可完全擦除」 的语法,enum、namespace、参数属性一律报错33。

把这五开关的「统一判据」提炼出来:它们全都在区分「剥掉类型就剩合法 JS」 与「必须生成额外代码」两种语句。Node 的擦除器只干第一件事; 你的 tsconfig 配置这五个开关,等于提前把代码里第二类的语句全部清出去。 这不只是「配合 Node 实验特性」的技巧——它本身就是第 10 章「写 JavaScript with types」纪律的编译器强制版。哪怕你短期不打算 node 直跑, 把这五开关当作代码风格的护栏来理解,也值得。

最后这个开关把第 10 章的「遗产特性」叙事接到了未来: Node 的类型擦除模型是「JavaScript with types」—— 只剥类型,不做变换。enum 那种需要生成运行代码的特性, 在这个模型里没有位置;支持它们意味着 Node 要内置一整套转译器, 维护成本陡增34。原书没下结论,但方向清楚:Node 的原生支持越成熟, 「可擦除」与「需变换」的分界就越会成为 TypeScript 语法的实际分界线—— erasableSyntaxOnly 就是提前站队。

判断(我们的,不是书里的): Node 原生类型擦除是 2026 年最值得盯的 趋势之一,但它的「严格五开关」组合(尤其 verbatimModuleSyntax 与 erasableSyntaxOnly)暴露了一个设计取向:未来的 TypeScript 代码 会更像「带标注的 JavaScript」,而不是「有自己的运行时特性的语言」。 对应用开发者,这个取向的实用推论是:现在写代码就按「可擦除」标准写 (import type 标清楚、不用 enum/namespace),未来切换到 node 直跑 或 anys 擦除工具链时零成本。 如果错,会错在: Node 的擦除支持在很长时期内会是「测试和脚本场景」的 便利,而非生产应用的路径(性能、生态、调试体验的成熟度都还需要时间); 「可擦除标准」因此可能长期只是洁癖而非刚需——但按它写代码没有额外成本, 这个建议仍然稳。

9. 多份 tsconfig:extends 与项目引用

TypeScript 怎么找 tsconfig

项目大了,一个 tsconfig 不够:同仓库里客户端要 DOM 类型、服务端不要。 规则一句话:每个 .ts 文件用离它最近的 tsconfig.json。 client/ 下的文件用 client/tsconfig.json,server/ 下的用 server 的, 其余用根目录的35:

project
 ├── client/tsconfig.json    ← lib: ["es2022", "dom", "dom.iterable"], jsx: "react-jsx"
 ├── server/tsconfig.json    ← lib: ["es2022"]
 └── tsconfig.json           ← 兜底

一个顺手的推论:全局声明也跟着配置走。server/server.d.ts 里 declare const ONLY_ON_SERVER: string,只在 server 配置的范围内可见; client 代码用它,报错36。这对「测试框架(Jest/Cypress)注入的全局」 「环境专属变量」极其有用——每个配置一个干净的全局命名空间, 互不污染。

'原书对多配置动机的概括也很准:「项目长大复杂后,同一仓库里有不同的 环境或目标」——client/server 是最常见的二元,但也有 app/test/e2e、lib/benchmark、web/extension 等变体。配置分裂的信号: 你发现自己在用 // @ts-ignore 压制「测试文件里才存在的全局」, 或者 lib 差异(该不该有 DOM)要靠注释协调——这两个信号出现任一个, 就是该拆 tsconfig 的时机。

把「全局跟配置走」的机制说透,因为它解释了太多日常现象:

  client/tsconfig.json 管辖 client/ 下所有文件
    → client/ 里声明的全局(含 .d.ts、declare global)只在这些文件里可见
  server/tsconfig.json 管辖 server/ 下所有文件
    → 各配置的全局命名空间彼此独立

  后果一:Jest 的 describe/it 全局只在「测试配置」里存在,
          应用代码不误认
  后果二:client 特有的 Window 补丁不会漏进 server 的类型
  后果三:在「错误」的配置文件里声明全局 = 全局不出现,
          排错时先看文件属于哪份 tsconfig

注意第三条的反向用法:如果你的全局声明「莫名不生效」, 第一件要查的事就是它所在文件属于哪份配置—— 这是多配置项目里排在首位的排错动作36。

extends:共享基线

两份配置大量重复?抽一个 tsconfig.base.json,各配置用 extends 引用:

// tsconfig.base.json
{ "compilerOptions": { "target": "es2022", "strict": true, "skipLibCheck": true, /* ... */ } }
// client/tsconfig.json
{
  "extends": "../tsconfig.base.json",
  "compilerOptions": { "lib": ["es2022", "dom", "dom.iterable"], "jsx": "react-jsx" }
}

基线一改,全员继承。注意一个坑:extends 只继承 compilerOptions—— include/exclude/files 这些「文件范围」设置不继承37。 子配置要自己声明自己管哪些文件。

这个坑的实际形态:在 base 里写了 "exclude": ["node_modules"], 子配置 extends 后惊觉 node_modules 又进来了——因为 exclude 没跟过来。 原书用一张注释表把这条钉死:

{
  "compilerOptions": {}, // Will be inherited by 'extends'
  "include": [],         // Will NOT be inherited by 'extends'
  "exclude": []          // Will NOT be inherited by 'extends'
}

所以多配置的纪律是:compilerOptions 进 base,文件范围留各自。 违反它不会让你立刻报错,只会让你在某个周一发现构建变慢了 (多检了一倍文件)——这是配置类 bug 的典型死法37。

项目引用:tsc -b 一键检查全部

多配置的日常烦恼:IDE 知道每份文件用哪份配置,但 CI 想一次检查整个项目 怎么办?逐个跑 tsc -p ./client/tsconfig.json && tsc -p ./server/tsconfig.json 很快就管不过来。项目引用(project references)是正式解:

// 根 tsconfig.json —— 只引用,不检查
{
  "references": [
    { "path": "./client/tsconfig.json" },
    { "path": "./server/tsconfig.json" }
  ],
  "files": []
}

根配置的 files: [] 让它自己不检查任何文件——它只是一张依赖图。 各子配置加 composite: true(「我是被引用的子项目」), 然后根目录一条命令:tsc -b——按依赖顺序检查并构建所有子项目, 还会在各自目录生成 .tsbuildinfo 缓存文件,加速后续构建38。

把这套配置从零搭一遍的顺序,值得作为操作手册记下来:

  第 1 步:根目录建 tsconfig.json,只写 references + "files": []
  第 2 步:各子项目(client/、server/)建自己的 tsconfig.json,
          extends 共享的 tsconfig.base.json,写自己的 lib 等差异项
  第 3 步:base 里加 "composite": true(每个子项目都经 base 继承到)
  第 4 步:根目录跑 tsc -b —— 按依赖图顺序构建,生成 .tsbuildinfo
  第 5 步:CI 里就一条 tsc -b;增量构建靠 .tsbuildinfo 续命

第一次跑通后,新增一个子项目(比如 packages/shared)只做三件事: 建 tsconfig、extends base、根的 references 加一行—— 其余(构建顺序、缓存、全局隔离)全自动38。

原书给的项目引用要点,收拢成六条:

  1. 一条命令跑多个 tsconfig;
  2. 每个子配置要 composite: true;
  3. 根配置用 references 指过去,且 files: [];
  4. tsc -b 从根运行;
  5. 每个配置有独立的全局作用域,全局不互通;
  6. .tsbuildinfo 加速增量构建39。

其中第 5 条和第 9 节的「全局跟配置走」是同一个特性,一体两面: 项目引用不只是构建顺序管理,还是全局命名空间的隔离墙—— 这正是「DOM 类型只给客户端、Jest 全局只给测试」能干净落地的原因40。

10. 这一章在全书链条中的位置

语言讲完、类型供给讲完,这一章把两者固化成配置:strict 与 noUncheckedIndexedAccess 是「检查人格」,module/ noEmit 是「转译人格」, lib 是「环境人格」,项目引用是「组织结构人格」。四份人格各管一段, 合起来就是 CI 上那个 tsc 的全部行为。它也回答了第 01 章埋的问题: 「TypeScript 的反馈环质量」不是玄学——严格开关每多开一个, IDE 红线的含金量就高一分;这两章(配置 + 声明文件)就是把含金量 调到最高的操作手册。

从「配置」回看全书,还能看到一个贯穿的结构:第 04 章的「参数必须注解」、 第 06 章的「收窄表态」、第 08 章的「可变性声明」、本章的「严格开关」—— 它们全是同一句话的不同刻度:TypeScript 的价值与「你向编译器 提供的信息量」成正比。配置只是这条曲线的最后一公里: 机制给了你能说多少,开关决定了你「必须」说多少。原书小结的排序:基线九项 → 四问追加 → 库与 monorepo 三件套 → 多配置与项目引用 → Node 原生支持41。

11. 作者的判断与证据(分开标)

书里有据的:

  • 基线九项与每项的作用——含各自挡下的事故类型42。
  • target 转译语法不补 API——replaceAll 的反例在案1。
  • isolatedModules 的「明智默认」定性——ambient const enum 的报错与产物对照67。
  • strict: false 的「分叉版本」定性及三条反驳(库、社区资源、锁死效应)—— 原书明确表态1011。
  • noUncheckedIndexedAccess 的代价坦承(「关了也不怪你」)——罕见的明确让步14。
  • NodeNext 的 .js 扩展名要求及理由(代码即产物)——机制解释在案17。
  • Node 擦除模型的「只剥类型、不做变换」设计取向——enum 变换产物对照34。
  • extends 只继承 compilerOptions 的陷阱——原书专门标注37。
  • 项目引用六要点——原书逐条列出39。

原书没说的(我们补):

  • noUncheckedIndexedAccess 与 strict 的「赋值诚实/访问诚实」分工—— 我们的归纳,判断块中已声明。
  • noUnusedLocals/Parameters 与 ESLint 的职责重叠——补充(来自通用知识): 2026 年多数团队用 ESLint 管未使用变量,规则粒度更细(如 _ 前缀豁免)。
  • verbatimModuleSyntax 在非 Node 场景的推广价值——补充: 该开关对 bundler 项目同样有益(明确类型导入对 isolatedModules 是补强), 部分团队已在全项目启用;Node 五开关只是把它变成了硬性要求。

判断(我们的,不是书里的): 这一章最反直觉的教诲是: 「配置即默认」比「配置即能力」重要。 团队真正遵循的从来不是 文档里写的规范,而是 tsc 默认放行的东西——strict 开着, 「必须处理 null」就是默认;noUncheckedIndexedAccess 开着, 「索引要表态」就是默认。反过来说,每一个没开的严格开关, 都是在给未来的自己签发一张「此时没炸、迟早要还」的借条。 选配置的完整问题不是「这个开关有什么用」,而是 「我希望团队把什么当作理所当然」——这是配置的人格化视角, 也是它和代码风格指南(ESLint、格式化)的本质区别:配置是可执行的。 如果错,会错在: 当团队对某类错误已有运行时防线(测试、监控、 灰度)且编译期检查的成本确实高于收益时,关某个开关是理性折中; 「全开最好」是默认值而非教条——但举证责任在「关」的那一方。

12. 边界与局限

  • 配置有时效:target 的 es2022、NodeNext 的行为、Node 原生支持的版本 要求(23+)都会随时间前移;抄基线时以当前文档为准,原书数字是 2025–2026 时点。
  • 项目引用的学习曲线真实存在:composite 的约束(如所有文件必须显式列入) 会让初次配置报错连连;小项目用 --project 逐个跑反而简单—— 项目引用是中大型 monorepo 的工具,不是默认必选项。
  • declarationMap 的「税」是隐性的:不开不会报错、不会变慢, 只是每天几十次跳转多走一段弯路——它的成本不在账单上,在注意力上; 评估「要不要开」时别用「有没有坏」用「顺不顺手」。
  • strict 的开与不开之间没有平滑档:strict 是总开关,内部单项可单独微调, 但「半严格」状态的组合未经社区充分验证,多数库按全 strict 测试—— 自定义组合的风险自担。
  • 基线的「九项」不是圣经,是 2026 年的快照:target 的具体版本、 NodeNext 的行为细节、Node 原生支持的成熟度,都会随时间演进; 本拆解与原书一样,给的是「配置的骨架 + 每项的判据」,数字以当下文档为准。
  • jsx 一节只覆盖了转译语义,React 类型本身(@types/react)的 组件签名、hooks 类型不在本章范围。

13. 可带走的

  1. 基线九项直接抄:skipLibCheck/target es2022/esModuleInterop/allowJs/ resolveJsonModule/moduleDetection force/isolatedModules/strict/ noUncheckedIndexedAccess。
  2. 追加四问:谁转译(NodeNext vs Preserve+noEmit)、DOM 吗(lib)、 是库吗(declaration)、monorepo 吗(composite+declarationMap)。
  3. strict 是地基,不是坡道——主流库、社区答案、长期可维护性都假定它开着。
  4. noUncheckedIndexedAccess:索引结果 T | undefined,啰嗦但诚实;默认开。
  5. target 管语法、lib 管 API——replaceAll 这类 API 缺口要 polyfill,别指望 target。
  6. isolatedModules 给「换转译器」买保险;禁用的特性(ambient const enum)本就该少用。
  7. NodeNext 导入要 .js 扩展名(代码即产物);Preserve 不用(打包器兜底)。
  8. 库的 declaration: true 自动产影子;monorepo 加 declarationMap 让点击直达源码。
  9. 多配置:就近原则;extends 只继承 compilerOptions;项目引用(tsc -b) 管顺序、做缓存、隔全局。
  10. Node 原生跑 TS = 类型擦除模型;五开关组合(尤其 verbatimModuleSyntax) 预告了「可擦除语法」的未来边界。
  11. 配置即人格:严格开关决定 IDE 红线的含金量。
  12. 表态的谱系:!、?.、if、默认值——noUncheckedIndexedAccess 只要求你选一种。
  13. tsc -b + references + composite:monorepo 的构建三件套,搭一次永久受益。
  14. isolatedModules 防的是「换转译器的静默损坏」——错误出现在它最便宜的地方。
  15. 全局声明不生效?先查它属于哪份 tsconfig。
  16. 配置即默认:团队遵循的是 tsc 放行的东西,不是文档写的规范。

最后给一张「接手陌生项目的配置体检单」,按顺序看四处,十分钟能摸完底:

  1. strict 开没开(以及 noUncheckedIndexedAccess 这个 strict 之外的常客)—— 它决定全项目红线的含金量,见第 3 节;
  2. module 阵营——NodeNext 还是 Preserve,判断转译归谁, 顺带看 import 有没有写 .js 扩展名、和阵营对不对得上,见第 6 节;
  3. noEmit 状态——跑一遍 tsc 看有没有多出一堆 .js 文件,就知道前任 有没有把「检查」和「产出」分干净,见第 6 节;
  4. 配置文件的组织——单配置、extends 链还是项目引用,决定改配置的爆炸半径, 见第 7、9 节。

四处看完,这个项目的类型纪律、工具链形态和改动风险,心里就有数了—— 比通读 tsconfig 文档快得多,也比问人可靠得多(配置现状常常和口口相传的不一样)。

14. 原文地图

主题 原书位置 原文位置
基线九项 Recommended Configuration text/30-fm-recommended-configuration.txt:23(搜「noUncheckedIndexedAccess」)
追加四问 同上 text/30-fm-recommended-configuration.txt:52(搜「NodeNext」)
target 不补 API 同上 text/30-fm-recommended-configuration.txt:137(搜「replaceAll」)
esModuleInterop 的定位 同上 text/30-fm-recommended-configuration.txt:147(搜「always be turned on」)
isolatedModules 与 const enum 同上 text/30-fm-recommended-configuration.txt:173(搜「Cannot access ambient const enums」)
strict 的成员示例 同上 text/30-fm-recommended-configuration.txt:192(搜「let name: string = null」)
strict: false 的「分叉」定性 同上 text/30-fm-recommended-configuration.txt:212(搜「fork of TypeScript」)
noUncheckedIndexedAccess 的崩溃反例 同上 text/30-fm-recommended-configuration.txt:247(搜「Cannot read property」)
关闭也不怪你 同上 text/30-fm-recommended-configuration.txt:269(搜「wouldn’t blame you」)
更多严格选项清单 同上 text/30-fm-recommended-configuration.txt:275(搜「allowUnreachableCode」)
NodeNext 与 .js 扩展名 同上 text/30-fm-recommended-configuration.txt:312(搜「./album.js」)
Node16 的 EOL 说明 同上 text/30-fm-recommended-configuration.txt:326(搜「end-of-life」)
Preserve 与打包器 同上 text/30-fm-recommended-configuration.txt:341(搜「./album」)
noEmit 默认 false 的坑 同上 text/30-fm-recommended-configuration.txt:357(搜「finding that running tsc emits」)
sourceMap 与 --enable-source-maps 同上 text/30-fm-recommended-configuration.txt:371(搜「enable-source-maps」)
outDir 与 rootDir 同上 text/30-fm-recommended-configuration.txt:399(搜「src/index.ts」)
declaration 自动产影子 同上 text/30-fm-recommended-configuration.txt:443(搜「export declare function」)
monorepo 跳转问题 同上 text/30-fm-recommended-configuration.txt:476(搜「taken to the .d.ts」)
declarationMap 的映射 同上 text/30-fm-recommended-configuration.txt:489(搜「original TypeScript source」)
jsx 三值 同上 text/30-fm-recommended-configuration.txt:504(搜「preserve」)
Node 直跑与 strip-types 同上 text/30-fm-recommended-configuration.txt:517(搜「experimental-strip-types」)
五开关组合 同上 text/30-fm-recommended-configuration.txt:527(搜「erasableSyntaxOnly」)
rewriteRelativeImportExtensions 同上 text/30-fm-recommended-configuration.txt:539(搜「./example.ts」)
verbatimModuleSyntax 的原因 同上 text/30-fm-recommended-configuration.txt:547(搜「runtime error」)
只剥类型不做变换 同上 text/30-fm-recommended-configuration.txt:574(搜「JavaScript with types」)
就近原则找 tsconfig 同上 text/30-fm-recommended-configuration.txt:588(搜「closest one」)
全局跟配置走 同上 text/30-fm-recommended-configuration.txt:631(搜「ONLY_ON_SERVER」)
extends 只继承 compilerOptions 同上 text/30-fm-recommended-configuration.txt:724(搜「Will NOT be inherited」)
项目引用与 tsc -b 同上 text/30-fm-recommended-configuration.txt:774(搜「tsc -b」)
.tsbuildinfo 缓存 同上 text/30-fm-recommended-configuration.txt:777(搜「tsbuildinfo」)
全局作用域隔离 同上 text/30-fm-recommended-configuration.txt:789(搜「not be shared」)
全局与配置的关系 同上 text/30-fm-recommended-configuration.txt:621(搜「tied to a single configuration」)
extends 的继承范围表 同上 text/30-fm-recommended-configuration.txt:724(搜「Will NOT be inherited」)
库产物目录四文件 同上 text/30-fm-recommended-configuration.txt:435(搜「album.d.ts」)
「配置即人格」的注脚 同上 text/30-fm-recommended-configuration.txt:204(搜「premise that you have strict enabled」)
本章小结 同上 text/30-fm-recommended-configuration.txt:797(搜「essential options」)

最后给一份「按项目类型的最终配方」,把全章压成三行:

  应用(前端框架托管):  基线九项 + module: Preserve + noEmit + lib 含 dom(+ jsx)
  应用(Node 后端):     基线九项 + module: NodeNext + outDir + sourceMap + lib es2022
  库(发 npm):         基线九项 + module: NodeNext + declaration + outDir
  库(monorepo 内部):   再加 composite + declarationMap

新增项目的第一小时,照着抄;三个月后的某次报错,回来查它属于哪一行—— 这就是本章设计的用法。


  1. 出处:「Recommended Configuration」第 109–139 段(text/30-fm-recommended-configuration.txt:137,搜「replaceAll」)。 ↩↩

  2. 出处:「Recommended Configuration」第 139 段(text/30-fm-recommended-configuration.txt:139,搜「configure the environment」)。 ↩

  3. 出处:「Recommended Configuration」第 141 段(text/30-fm-recommended-configuration.txt:141,搜「keep it up to date」)。 ↩

  4. 出处:「Recommended Configuration」第 143–147 段(text/30-fm-recommended-configuration.txt:147,搜「always be turned on」)。 ↩

  5. 出处:「Recommended Configuration」第 149–153 段(text/30-fm-recommended-configuration.txt:151,搜「single-file transpilers」)。 ↩

  6. 出处:「Recommended Configuration」第 155–182 段(text/30-fm-recommended-configuration.txt:173,搜「Cannot access ambient const enums」)。 ↩↩↩

  7. 出处:「Recommended Configuration」第 184 段(text/30-fm-recommended-configuration.txt:184,搜「sensible default」)。 ↩↩

  8. 出处:「Recommended Configuration」第 186–193 段(text/30-fm-recommended-configuration.txt:188,搜「shorthand」)。 ↩

  9. 出处:「Recommended Configuration」第 190–202 段(text/30-fm-recommended-configuration.txt:192,搜「let name: string = null」)。 ↩

  10. 出处:「Recommended Configuration」第 206–210 段(text/30-fm-recommended-configuration.txt:208,搜「zod」)。 ↩↩

  11. 出处:「Recommended Configuration」第 212 段(text/30-fm-recommended-configuration.txt:212,搜「fork of TypeScript」)。 ↩↩

  12. 出处:「Recommended Configuration」第 214–247 段(text/30-fm-recommended-configuration.txt:247,搜「Cannot read property」)。 ↩

  13. 出处:「Recommended Configuration」第 249–265 段(text/30-fm-recommended-configuration.txt:263,搜「possibly 'undefined'」)。 ↩

  14. 出处:「Recommended Configuration」第 267–269 段(text/30-fm-recommended-configuration.txt:269,搜「wouldn’t blame you」)。 ↩↩

  15. 出处:「Recommended Configuration」第 271–289 段(text/30-fm-recommended-configuration.txt:275,搜「allowUnreachableCode」)。 ↩

  16. 出处:「Recommended Configuration」第 291–314 段(text/30-fm-recommended-configuration.txt:307,搜「emulates Node」)。 ↩↩

  17. 出处:「Recommended Configuration」第 309–314 段(text/30-fm-recommended-configuration.txt:312,搜「./album.js」)。 ↩↩↩

  18. 出处:「Recommended Configuration」第 316–326 段(text/30-fm-recommended-configuration.txt:326,搜「end-of-life」)。 ↩

  19. 出处:「Recommended Configuration」第 328–343 段(text/30-fm-recommended-configuration.txt:341,搜「./album」)。 ↩↩

  20. 出处:「Recommended Configuration」第 345–357 段(text/30-fm-recommended-configuration.txt:357,搜「finding that running tsc emits」)。 ↩

  21. 出处:「Recommended Configuration」第 359–372 段(text/30-fm-recommended-configuration.txt:371,搜「enable-source-maps」)。 ↩

  22. 出处:「Recommended Configuration」第 378–399 段(text/30-fm-recommended-configuration.txt:399,搜「src/index.ts」)。 ↩

  23. 出处:「Recommended Configuration」第 401–460 段(text/30-fm-recommended-configuration.txt:443,搜「export declare function」)。 ↩

  24. 出处:「Recommended Configuration」第 462–476 段(text/30-fm-recommended-configuration.txt:476,搜「taken to the .d.ts」)。 ↩

  25. 出处:「Recommended Configuration」第 478–491 段(text/30-fm-recommended-configuration.txt:489,搜「original TypeScript source」)。 ↩

  26. 出处:「Recommended Configuration」第 491 段(text/30-fm-recommended-configuration.txt:491,搜「quality-of-life」)。 ↩

  27. 出处:「Recommended Configuration」第 493–508 段(text/30-fm-recommended-configuration.txt:504,搜「Keeps JSX syntax」)。 ↩↩

  28. 出处:「Recommended Configuration」第 510–517 段(text/30-fm-recommended-configuration.txt:517,搜「experimental-strip-types」)。 ↩

  29. 出处:「Recommended Configuration」第 531–535 段(text/30-fm-recommended-configuration.txt:533,搜「Node’s module system」)。 ↩

  30. 出处:「Recommended Configuration」第 535–536 段(text/30-fm-recommended-configuration.txt:535,搜「emit any files」)。 ↩

  31. 出处:「Recommended Configuration」第 537–540 段(text/30-fm-recommended-configuration.txt:539,搜「./example.ts」)。 ↩

  32. 出处:「Recommended Configuration」第 542–548 段(text/30-fm-recommended-configuration.txt:547,搜「runtime error」)。 ↩

  33. 出处:「Recommended Configuration」第 549–576 段(text/30-fm-recommended-configuration.txt:576,搜「erasableSyntaxOnly」)。 ↩

  34. 出处:「Recommended Configuration」第 566–574 段(text/30-fm-recommended-configuration.txt:574,搜「JavaScript with types」)。 ↩↩

  35. 出处:「Recommended Configuration」第 578–599 段(text/30-fm-recommended-configuration.txt:588,搜「closest one」)。 ↩

  36. 出处:「Recommended Configuration」第 619–633 段(text/30-fm-recommended-configuration.txt:631,搜「ONLY_ON_SERVER」)。 ↩↩

  37. 出处:「Recommended Configuration」第 635–726 段(text/30-fm-recommended-configuration.txt:724,搜「Will NOT be inherited」)。 ↩↩↩

  38. 出处:「Recommended Configuration」第 728–777 段(text/30-fm-recommended-configuration.txt:774,搜「tsc -b」)、第 777 段(搜「tsbuildinfo」)。 ↩↩

  39. 出处:「Recommended Configuration」第 779–791 段(text/30-fm-recommended-configuration.txt:779,搜「sum up」)。 ↩↩

  40. 出处:「Recommended Configuration」第 789、793 段(text/30-fm-recommended-configuration.txt:789,搜「not be shared」)。 ↩

  41. 出处:「Recommended Configuration」第 795–807 段(text/30-fm-recommended-configuration.txt:797,搜「essential options」)。 ↩

  42. 出处:「Recommended Configuration」第 8–101 段(text/30-fm-recommended-configuration.txt:26,搜「what each setting does」)。 ↩

  43. 出处:「Recommended Configuration」第 204 段(text/30-fm-recommended-configuration.txt:204,搜「premise that you have strict enabled」)。原文:全书都建立在「代码库开着 strict」的前提上。 ↩