TypeScript 的 strict 是一组编译器检查开关,用来拒绝无法证明安全的赋值、调用和属性读取。
strict: true 不会验证 JSON,也不包含 noUncheckedIndexedAccess 等所有强化选项;断言还可以绕过它发现的问题。
在项目配置中启用 strict,让外部数据以 unknown 进入,并通过收窄、初始化和测试解决每个错误。
是什么,为什么存在
TypeScript 的 严格模式(strict mode) 不是一种运行模式,而是一组互相关联的静态检查。配置 strict: true 后,编译器会对空值、函数参数、类字段、this、捕获变量和未能推断类型的位置采用更保守的规则。
TypeScript 必须接纳大量已有 JavaScript,因此它允许项目逐步增加类型约束。宽松配置可以让旧代码先编译,却也会把一些未经证明的假设当成合法代码。例如,查找操作可能没有结果,回调可能只接受更窄的输入,类字段也可能在读取时仍未初始化。
严格模式把这些假设变成编译错误,让修复发生在代码运行之前。你会在新建 tsconfig.json、接管旧项目、升级编译器,或者为缺少类型声明的库补边界时遇到它。它并不要求每个局部变量都写注解;只要推断结果足够明确,代码仍可保持简洁。
这些保证在编译阶段结束。TypeScript 会进行 类型擦除(type erasure) ,因此生成的 JavaScript 不会携带接口、联合类型或大多数检查。严格模式能约束经过检查的源码,却不能证明网络响应、文件内容或 JavaScript 调用方送来的值符合声明。
工作原理
strict 是一个总开关。TypeScript 6 会通过它启用九个相关选项,但你仍可在同一份配置中把某个子选项显式设为 false。后写的例外不会关闭总开关的其他部分。
{
"compilerOptions": {
"strict": true
}
}TypeScript 6 中的严格选项如下。以后版本可能在 strict 下加入更严格的检查,所以编译器升级有可能带来新的诊断。
| 选项 | 启用后的变化 |
|---|---|
noImplicitAny | 报告无法推断而产生的 隐式 any(implicit any) 。 |
noImplicitThis | 当 this 只能得到 any 类型时报告错误。 |
strictNullChecks | 让 null 和 undefined 成为必须显式处理的独立类型。 |
strictFunctionTypes | 赋值函数类型时,更安全地检查参数与返回值的兼容性。 |
strictBindCallApply | 按原函数签名检查 bind、call 和 apply 的参数。 |
strictPropertyInitialization | 要求类实例字段在声明处或构造路径中完成初始化。 |
strictBuiltinIteratorReturn | 让内置迭代器的完成返回值默认为 undefined,而不是 any。 |
useUnknownInCatchVariables | 让未标注的 catch 变量采用 unknown,使用前必须收窄。 |
alwaysStrict | 把源码按 ECMAScript 严格模式解析,并在需要时输出 "use strict"。 |
这些选项改变的是编译器需要什么证据。strictNullChecks 会让查找函数的 undefined 出现在返回类型中;一次存在性检查随后触发 控制流收窄(control-flow narrowing) 。strictPropertyInitialization 则分析构造路径,而不是等对象第一次使用时再发现字段缺失。
noImplicitAny 只禁止编译器悄悄推断出的 any,并不禁止作者显式写 any。类似地,useUnknownInCatchVariables 会保护默认的捕获变量,但 catch (error: any) 仍能主动退出检查。严格模式提高默认门槛,无法阻止代码明确绕过门槛。
示例
下面四个程序都使用 TypeScript 6.0.3 以 strict: true 完成类型检查,再由 Node 24 的 tsx 执行。它们依次展示空值、回调参数、运行时边界和内置迭代器。
收窄可空查找结果
数组的 find() 可能返回 undefined。严格空值检查把这种可能性保留在类型中,因此读取账户属性前必须处理未找到的分支。
type Account = { id: number; displayName: string | null };
const accounts: Account[] = [
{ id: 1, displayName: "Ada" },
{ id: 2, displayName: null },
];
function findAccount(id: number): Account | undefined {
return accounts.find((account) => account.id === id);
}
function labelFor(id: number): string {
const account = findAccount(id);
if (account === undefined) return "missing";
return account.displayName ?? `account-${account.id}`;
}
console.log(labelFor(1));
console.log(labelFor(2));
console.log(labelFor(3));Ada
account-2
missingaccount === undefined 之后,编译器知道剩余路径中的 account 是 Account。displayName 仍可能为 null,所以空值合并运算符提供与数据模型一致的后备标签。
这里没有使用非空断言。写成 findAccount(id)!.displayName 虽然能消除诊断,却会在编号不存在时重新引入运行时错误。
保持回调参数安全
函数赋值必须考虑调用方允许传入的全部值。接受任意 Animal 的函数可以放到只会收到 Dog 的位置;只会处理 Dog 的函数不能冒充通用处理器。
interface Animal {
name: string;
}
interface Dog extends Animal {
bark(): string;
}
type AnimalHandler = (animal: Animal) => string;
type DogHandler = (dog: Dog) => string;
const describeAnimal: AnimalHandler = (animal) => `Checking ${animal.name}`;
const describeDog: DogHandler = (dog) => `${dog.name} says ${dog.bark()}`;
const handleDog: DogHandler = describeAnimal;
// @ts-expect-error 只处理 Dog 的函数不能安全处理所有 Animal。
const handleEveryAnimal: AnimalHandler = describeDog;
const pixel: Dog = { name: "Pixel", bark: () => "woof" };
console.log(handleDog(pixel));
console.log(describeDog(pixel));Checking Pixel
Pixel says woof@ts-expect-error 在这里是一项编译测试:如果不安全赋值不再产生诊断,指令本身就会报错。示例没有调用被拒绝的赋值,因此运行结果只来自两个安全调用。
这条规则属于 函数参数变型(function parameter variance) 。方向容易看反:判断的不是函数名称里写了哪种动物,而是接收该函数的调用方以后可以传入什么。
验证边界并初始化字段
严格检查无法验证 JSON,但可以迫使边界代码保留未知性。下面先把解析结果赋给 unknown,检查对象形状和领域约束,再创建字段已经完整初始化的实例。
class RetryPolicy {
constructor(
readonly endpoint: string,
readonly maxAttempts: number,
) {}
}
function parsePolicy(text: string): RetryPolicy {
const value: unknown = JSON.parse(text);
if (
typeof value !== "object" ||
value === null ||
!("endpoint" in value) ||
!("maxAttempts" in value) ||
typeof value.endpoint !== "string" ||
typeof value.maxAttempts !== "number" ||
!Number.isInteger(value.maxAttempts) ||
value.maxAttempts < 1
) {
throw new Error("Invalid retry policy");
}
return new RetryPolicy(value.endpoint, value.maxAttempts);
}
for (const raw of [
'{"endpoint":"/v1/retry","maxAttempts":3}',
'{"endpoint":"/v1/retry","maxAttempts":0}',
]) {
try {
const policy = parsePolicy(raw);
console.log(`policy=${policy.endpoint}:${policy.maxAttempts}`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.log(`error=${message}`);
}
}policy=/v1/retry:3
error=Invalid retry policy构造函数参数属性在对象返回前完成赋值,所以 strictPropertyInitialization 可以证明两个字段都存在。这里不需要 明确赋值断言(definite assignment assertion) ,也没有一个可被提前读取的半初始化实例。
catch 变量是 unknown,因为 JavaScript 可以抛出字符串、对象或 null,并不只会抛出 Error。instanceof Error 分支保留标准错误消息,后备分支则安全地把其他值转成字符串。
检查迭代器是否结束
内置迭代器的 next() 同时返回完成标记和值。strictBuiltinIteratorReturn 阻止完成分支中的 value 以 any 混入程序,读取前要先判断 done。
function printJobs(jobs: Set<string>): void {
const iterator = jobs.values();
while (true) {
const next = iterator.next();
if (next.done) break;
console.log(next.value.toUpperCase());
}
}
printJobs(new Set(["compile", "test"]));
console.log("done");COMPILE
TEST
done检查 next.done 后,TypeScript 能在未完成分支中确定 next.value 是 string。直接访问 jobs.values().next().value.toUpperCase() 会忽略空集合,并在严格检查下暴露出可能的 undefined。
这类诊断常出现在编译器升级之后,因为旧版本曾让内置迭代器的完成值默认为 any。修复应遵守迭代器协议,而不是在 value 后添加 !。
陷阱
修复方法: 让不可信值以 unknown 进入,检查容器类型、必填字段和领域约束,再构造领域对象。类型断言只记录已经完成的证明,不能代替证明。
修复方法: 按错误来源修复:补充边界类型,收窄联合,覆盖缺失分支,或在构造函数中初始化字段。确实需要逃生口时,把它限制在一个小型适配器中,并记录它依赖的运行时条件。
修复方法: 根据项目契约另外评估 noUncheckedIndexedAccess、exactOptionalPropertyTypes 和 noImplicitOverride。它们会带来真实迁移成本,所以应逐项启用并用测试确认行为,而不是把它们误称为 strict 的组成部分。
修复方法: 在持续集成中运行明确的项目命令,例如 tsc -p tsconfig.json --noEmit,并检查 tsc --showConfig -p tsconfig.json 的最终配置。配置使用 extends 时,还要寻找下游对严格子选项的覆盖。
修复方法: 对回调契约使用函数属性,例如 handle: (event: Event) => void,让严格函数检查生效。来自第三方方法的值仍要按其真实输入范围测试,不能只依赖一次结构赋值。
严格模式的边界
严格模式提供一组实用的默认检查,但它不能证明程序正确。只有理解它留下的检查空白,才能判断一次无诊断构建究竟证明了什么。
总开关不是最高强度
strict 的选项集合会随 TypeScript 版本发展。TypeScript 6 包含 strictBuiltinIteratorReturn,所以从更早版本升级时,即使 tsconfig.json 没有改动,也可能出现新的错误。这是总开关的设计结果,不是编译器忽略了配置。
项目常在 strict 之外再选择三个强化选项。它们不是无条件的最佳组合,而是针对不同契约补上检查空白。
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true
}
}noUncheckedIndexedAccess 会把未声明索引的结果扩成可能的 undefined。exactOptionalPropertyTypes 区分属性缺失与显式写入 undefined,除非属性类型本身允许后者。noImplicitOverride 要求覆盖基类成员时写出 override,从而发现拼写或重构造成的意外新成员。
这些选项会接触不同代码形态,适合分别评估。启用后应保留能触发新边界的测试;否则团队很容易用断言恢复旧行为,却失去开启选项的目的。
函数属性与方法例外
严格函数类型检查主要作用于函数类型的赋值。对参数位置而言,接收更宽类型的函数可以替代接收更窄类型的函数,因为调用方只会提供那个窄集合中的值。相反替代则可能收到它无法处理的对象。
TypeScript 对方法语法保留兼容性例外。接口中的 compare(a: T, b: T): number 属于方法,而 compare: (a: T, b: T) => number 属于函数属性;后者接受严格的参数检查。这个差别常藏在库声明、事件接口和由生成工具改写的类型中。
例外不代表方法调用一定会失败,只表示某些不安全赋值不会被这个选项拒绝。设计新的回调 API 时,函数属性能表达更强的检查意图;实现现有方法接口时,则要用较宽输入的测试补上静态空白。
配置继承后的真实值
真实项目常通过 extends 组合配置。基础配置可以启用 strict,下游配置仍能关闭一个子选项;审查时只看基础文件,会把实际检查强度判断错。
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"strictNullChecks": false
}
}下游的 strictNullChecks: false 会覆盖总开关对这一项的默认启用,但其他严格子选项仍然生效。这样的例外有时用于迁移,却也容易永久留在配置中,让团队误以为整个项目已经完成空值检查。
tsc --showConfig -p tsconfig.json 会输出解析后的配置和文件集合,适合确认继承、默认值与目标文件。它展示的是编译器实际读取的结果,比人工拼接多层 JSON 更可靠。
持续集成还应使用同一条 -p 命令。编辑器可能选择更近的配置,构建工具也可能传入覆盖参数;只有统一入口,开发环境中的绿色诊断才与发布门禁表示同一件事。
检查空白仍然存在
显式 any 是严格模式保留的逃生口。值一旦成为 any,属性访问、调用和赋值会跳过大部分证明,而且不安全类型可能沿返回值继续传播。边界上的一个 any 往往比函数内部的一条错误更难发现。
审查逃生口时,可以按它跳过的证明类型分类:
| 写法 | 跳过的检查 | 需要补充的证据 |
|---|---|---|
显式 any | 后续大部分类型操作 | 边界验证与传播范围 |
value as Target | 本次可赋值性判断 | 断言前的运行时检查 |
@ts-expect-error | 下一行的已知诊断 | 预期错误与删除条件 |
skipLibCheck | 声明文件之间的检查 | 依赖版本与集成测试 |
类型断言也能越过可赋值性检查。断言适合表达编译器无法推导、而程序已经在运行时验证的事实;如果验证不存在,它只是把风险从诊断列表移到执行路径。
@ts-expect-error 和 @ts-ignore 在下一行抑制诊断,skipLibCheck 则跳过声明文件之间的类型检查。它们解决的范围不同,不能互相当作替代品,也不能证明被跳过的声明与运行库一致。
缺少逃生口清单时,无诊断构建并不能说明太多。代码审查可以要求每个抑制说明预期诊断、外部依赖和删除条件,并用测试覆盖编译器没有检查的那条路径。
两种严格模式不是一回事
alwaysStrict 涉及 ECMAScript 的运行时严格语义,例如禁止某些静默失败和旧语法。TypeScript 的 strict 总开关同时包含它,但本页其余选项主要改变静态类型检查。
不要根据输出中是否出现 "use strict" 判断类型检查是否开启。模块目标、发射方式和源文件形式会影响指令是否需要写出,而最终编译器配置才决定 strictNullChecks 等检查是否生效。
反过来也一样:一段 JavaScript 在 ECMAScript 严格模式下运行,不表示它接受过 TypeScript 严格类型检查。两种机制名称相似,检查阶段和保证范围却不同。
渐进迁移仍要保留边界
旧项目无法一次修完所有严格错误时,可以用多个项目配置或包边界分批推进。迁移单位应当能独立检查,而且新严格区域不能通过未标注导出把 any 重新传播给其他代码。
临时抑制应当可搜索、可解释并带有删除条件。@ts-expect-error 比 @ts-ignore 更适合已知诊断,因为错误消失后它会反过来失败;但它仍应靠近一条具体兼容性断言,而不是覆盖大段业务逻辑。
先修复公共输入输出和数据边界,通常能减少后续错误的扩散。大量叶子函数报错时,根因往往是上游参数或库声明已经变成 any,逐个给叶子添加断言只会隐藏这条传播路径。
迁移完成的证据不是错误计数归零,而是目标配置确实生效,逃生口有清楚范围,关键边界有运行时测试。更细的分批方案属于渐进迁移主题;严格模式在这里负责定义最终检查基线。
延伸阅读
4个问题 · 1 道输出预测题 · 1 道找错题