这节在地图里是工程化的"总开关房"。第一章只给了最小 tsconfig,这里展开真正影响契约严格度与编译速度的那些项。读懂它们,你才调得动团队的统一口径。
strict: true 是七个子项的合集。理解每个子项,才能针对老项目精准开启。
{ "compilerOptions": { "strict": true, "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "noImplicitThis": true, "alwaysStrict": true } }
各子项作用:
// strictNullChecks:null/undefined 不再随意赋给其它类型 let s: string = null; // 报错(开启后) // strictFunctionTypes:函数参数按逆变检查(见 2.4) type F = (x: Animal) => void; const g: (x: Dog) => void = (x: Animal) => {}; // 开启后才严格 // strictPropertyInitialization:类属性必须初始化 class C { name: string; // 报错:未初始化也未声明可选 constructor() {} // 需 name = "" 或 name?: string }
输入输出:关 strictNullChecks 时 let s: string = null 通过,开着就拦——这正是"契约要不要管空值"的开关。
默认 arr[0] 被当作 T 而非 T | undefined,但越界时是 undefined。开启此项让索引访问也变可选。
const arr: number[] = [1]; const x = arr[0]; // 开启后 x: number | undefined if (x !== undefined) x.toFixed(1);
大项目每次全量检查很慢。用 incremental + tsBuildInfoFile 缓存上次的依赖图。
{ "compilerOptions": { "incremental": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", "composite": true } }
composite 是项目引用的前提(多包仓库),要求开启 incremental。这张图标出开关对"编译速度 vs 严格度"的拉扯:

{ "include": ["src"], "exclude": ["node_modules", "dist", "**/*.test.ts"] }
exclude 不会让被 import 的文件逃过检查——只要被 include 内的文件引用,仍会被编译。想彻底跳过某文件用 // @ts-nocheck。
大仓里用 references 让子项目互相看到类型而不必打包。
// 根 tsconfig.json { "files": [], "references": [ { "path": "./packages/core" }, { "path": "./packages/web" } ] }
背景:monorepo 里 web 依赖 core 的类型。操作:core 开 composite,根配 references。结果:web 编译时能拿到 core 的 .d.ts,不用先构建 core。解读:这是类型层面的"软依赖",详见构建集成一节。
skipLibCheck 到底跳过了什么它只跳过 node_modules 里 .d.ts 的类型检查(第三方库声明),不碰你的源码,也不会让你的代码变宽松——所以它是安全的提速项。
{ "compilerOptions": { "skipLibCheck": true } }
背景:某些老库声明写得不严谨,全量检查会爆一堆与你不相关的错。操作:开 skipLibCheck。结果:检查时间大幅下降,你的 src 仍被完整检查。解读:它不是"关类型检查"的开关,而是"别替别人擦屁股"的开关,和降低严格度是两回事。频繁看到第三方 .d.ts 报错时,第一反应应是它,而非放宽自己。
target 与 lib 的配合target 决定编译输出到哪个 ES 版本,lib 决定你能在代码里用哪些内置类型。两者不匹配会"编译报缺定义"。
{ "compilerOptions": { "target": "ES2020", "lib": ["ES2020", "DOM"], "module": "ESNext" } }
背景:写 Array.prototype.at(ES2022)却 target: ES2017,编译会报"at 不存在"。操作:把 lib 提到包含该 API 的版本,或升 target。结果:类型与运行时能力对齐。解读:target 管降级产出、lib 管可用 API 类型,二者要同步;只在浏览器跑的项目加 "DOM" 才能用 document 等类型。
verbatimModuleSyntax:让类型导入显式化开启后,类型导入必须用 import type,值和类型在语法上彻底分开,配合打包器能更精准树摇。
// 开启 verbatimModuleSyntax 后 import type { User } from "./models"; // 类型:必须加 type import { saveUser } from "./api"; // 值:正常 import // 若把类型写成普通 import 会报错,强制你区分
背景:打包器分不清"这个 import 是值还是类型",可能保留无用代码。操作:开启该开关 + 用 import type。结果:编译产物更干净,类型依赖一眼可辨。解读:这是严格工程化(第四章基调)的进一步收紧,库作者对它收益最大。
老项目别一把 strict: true 砸下去,按这个顺序逐步吃:
# 第一步:只开 null 检查,修一批最危险的错误 # 第二步:开 noImplicitAny # 第三步:开 strictFunctionTypes # 最后:strict: true 收尾
每开一项就修一波,CI 绿了再开下一项。这样错误量可控,团队不崩溃。
lib 决定哪些内置类型可用(如 Promise、Map),target 决定语法降级到哪一代。两者错配会出诡异报错。
// 场景:target ES5 但代码用了 Promise,且没声明 lib { "compilerOptions": { "target": "ES5" // 未写 lib,则默认按 target 给,ES5 默认不含 Promise 类型 } } // 修正:显式加 lib,或把 target 提到 ES2017+ { "compilerOptions": { "target": "ES5", "lib": ["ES2017", "DOM"] } }
输入输出:缺 lib 时 new Promise(...) 报"找不到名称 Promise";补上 ["ES2017"] 后识别。注意 lib 只补类型,运行时的 Promise 仍需目标环境支持或用 polyfill,类型与运行时要分开看。
新版本引入 verbatimModuleSyntax,强制区分"值 import"与"类型 import",避免误把类型当值打进产物。
{ "compilerOptions": { "verbatimModuleSyntax": true } }
开启后,凡只用类型的导入必须写 import type,否则报错。它能让打包器的摇树更准,也逼迫团队显式标注类型依赖——代价是老代码要补一堆 import type。解读:这是"类型与值边界"在配置层的强化,配合第四章工程化纪律很合适。
strict 是七项合集,可逐项精准开启noUncheckedIndexedAccess 让数组取值也防空incremental/skipLibCheck 提速但不降严格度⚠️ 别用关检查来提速。skipLibCheck 跳过的是第三方声明(通常不归你管),不是你的代码;真正该提速用 incremental,别动严格度。
💡 迁移老项目时,先把 strictNullChecks 单独开起来,它拦下的空值错误往往是最常见也最致命的一类,性价比最高。