# 严格模式

Source: https://codewiki.com/zh/typescript/strict-mode/

> - **what**: TypeScript 的 `strict` 是一组编译器检查开关，用来拒绝无法证明安全的赋值、调用和属性读取。
> - **trap**: `strict: true` 不会验证 JSON，也不包含 `noUncheckedIndexedAccess` 等所有强化选项；断言还可以绕过它发现的问题。
> - **fix**: 在项目配置中启用 `strict`，让外部数据以 `unknown` 进入，并通过收窄、初始化和测试解决每个错误。

## 是什么，为什么存在

TypeScript 的严格模式（strict mode）不是一种运行模式，而是一组互相关联的静态检查。配置 `strict: true` 后，编译器会对空值、函数参数、类字段、`this`、捕获变量和未能推断类型的位置采用更保守的规则。

TypeScript 必须接纳大量已有 JavaScript，因此它允许项目逐步增加类型约束。宽松配置可以让旧代码先编译，却也会把一些未经证明的假设当成合法代码。例如，查找操作可能没有结果，回调可能只接受更窄的输入，类字段也可能在读取时仍未初始化。

严格模式把这些假设变成编译错误，让修复发生在代码运行之前。你会在新建 `tsconfig.json`、接管旧项目、升级编译器，或者为缺少类型声明的库补边界时遇到它。它并不要求每个局部变量都写注解；只要推断结果足够明确，代码仍可保持简洁。

这些保证在编译阶段结束。TypeScript 会进行类型擦除（type erasure），因此生成的 JavaScript 不会携带接口、联合类型或大多数检查。严格模式能约束经过检查的源码，却不能证明网络响应、文件内容或 JavaScript 调用方送来的值符合声明。

## 工作原理

`strict` 是一个总开关。TypeScript 6 会通过它启用九个相关选项，但你仍可在同一份配置中把某个子选项显式设为 `false`。后写的例外不会关闭总开关的其他部分。

```json
{
  "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`。严格空值检查把这种可能性保留在类型中，因此读取账户属性前必须处理未找到的分支。

<!-- quick -->

```typescript
// file: nullable-user.ts
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));
```

```text
Ada
account-2
missing
```

<!-- /quick -->

`account === undefined` 之后，编译器知道剩余路径中的 `account` 是 `Account`。`displayName` 仍可能为 `null`，所以空值合并运算符提供与数据模型一致的后备标签。

这里没有使用非空断言。写成 `findAccount(id)!.displayName` 虽然能消除诊断，却会在编号不存在时重新引入运行时错误。

### 保持回调参数安全

函数赋值必须考虑调用方允许传入的全部值。接受任意 `Animal` 的函数可以放到只会收到 `Dog` 的位置；只会处理 `Dog` 的函数不能冒充通用处理器。

```typescript
// file: callback-variance.ts
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));
```

```text
Checking Pixel
Pixel says woof
```

`@ts-expect-error` 在这里是一项编译测试：如果不安全赋值不再产生诊断，指令本身就会报错。示例没有调用被拒绝的赋值，因此运行结果只来自两个安全调用。

这条规则属于函数参数变型（function parameter variance）。方向容易看反：判断的不是函数名称里写了哪种动物，而是接收该函数的调用方以后可以传入什么。

### 验证边界并初始化字段

严格检查无法验证 JSON，但可以迫使边界代码保留未知性。下面先把解析结果赋给 `unknown`，检查对象形状和领域约束，再创建字段已经完整初始化的实例。

```typescript
// file: retry-policy.ts
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}`);
  }
}
```

```text
policy=/v1/retry:3
error=Invalid retry policy
```

构造函数参数属性在对象返回前完成赋值，所以 `strictPropertyInitialization` 可以证明两个字段都存在。这里不需要明确赋值断言（definite assignment assertion），也没有一个可被提前读取的半初始化实例。

`catch` 变量是 `unknown`，因为 JavaScript 可以抛出字符串、对象或 `null`，并不只会抛出 `Error`。`instanceof Error` 分支保留标准错误消息，后备分支则安全地把其他值转成字符串。

### 检查迭代器是否结束

内置迭代器的 `next()` 同时返回完成标记和值。`strictBuiltinIteratorReturn` 阻止完成分支中的 `value` 以 `any` 混入程序，读取前要先判断 `done`。

```typescript
// file: iterator-result.ts
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");
```

```text
COMPILE
TEST
done
```

检查 `next.done` 后，TypeScript 能在未完成分支中确定 `next.value` 是 `string`。直接访问 `jobs.values().next().value.toUpperCase()` 会忽略空集合，并在严格检查下暴露出可能的 `undefined`。

这类诊断常出现在编译器升级之后，因为旧版本曾让内置迭代器的完成值默认为 `any`。修复应遵守迭代器协议，而不是在 `value` 后添加 `!`。

## 陷阱

> **陷阱:** 把 `strict` 当作运行时验证器。`JSON.parse(raw) as User`、`response.json() as User` 和来自 JavaScript 的实参都不会因严格模式而接受结构检查。

**修复方法：** 让不可信值以 `unknown` 进入，检查容器类型、必填字段和领域约束，再构造领域对象。类型断言只记录已经完成的证明，不能代替证明。

> **陷阱:** 为了清空错误列表而批量加入 `any`、`as Target`、非空断言 `!` 或明确赋值断言 `field!`。这些写法会压制编译器正在报告的不确定性。

**修复方法：** 按错误来源修复：补充边界类型，收窄联合，覆盖缺失分支，或在构造函数中初始化字段。确实需要逃生口时，把它限制在一个小型适配器中，并记录它依赖的运行时条件。

> **陷阱:** 以为 `strict: true` 已经包含所有最严格的检查。数组越界、可选属性的精确写入语义和覆盖方法拼写并不全部由这个总开关处理。

**修复方法：** 根据项目契约另外评估 `noUncheckedIndexedAccess`、`exactOptionalPropertyTypes` 和 `noImplicitOverride`。它们会带来真实迁移成本，所以应逐项启用并用测试确认行为，而不是把它们误称为 `strict` 的组成部分。

> **陷阱:** 编辑了 `tsconfig.json`，实际检查命令却没有使用它。直接给 `tsc` 传入单个文件，或从错误目录选择另一份项目配置，都可能让本地实验与构建结果不同。

**修复方法：** 在持续集成中运行明确的项目命令，例如 `tsc -p tsconfig.json --noEmit`，并检查 `tsc --showConfig -p tsconfig.json` 的最终配置。配置使用 `extends` 时，还要寻找下游对严格子选项的覆盖。

> **陷阱:** 认为 `strictFunctionTypes` 会同样约束接口中的方法语法。为了兼容常见类和 DOM 层次，方法声明的参数仍允许双向兼容，因此更窄的方法可能穿过一次看似安全的赋值。

**修复方法：** 对回调契约使用函数属性，例如 `handle: (event: Event) => void`，让严格函数检查生效。来自第三方方法的值仍要按其真实输入范围测试，不能只依赖一次结构赋值。

<!-- deep -->

## 严格模式的边界

严格模式提供一组实用的默认检查，但它不能证明程序正确。只有理解它留下的检查空白，才能判断一次无诊断构建究竟证明了什么。

### 总开关不是最高强度

`strict` 的选项集合会随 TypeScript 版本发展。TypeScript 6 包含 `strictBuiltinIteratorReturn`，所以从更早版本升级时，即使 `tsconfig.json` 没有改动，也可能出现新的错误。这是总开关的设计结果，不是编译器忽略了配置。

项目常在 `strict` 之外再选择三个强化选项。它们不是无条件的最佳组合，而是针对不同契约补上检查空白。

```json
{
  "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`，下游配置仍能关闭一个子选项；审查时只看基础文件，会把实际检查强度判断错。

```json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}
```

```json
{
  "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`，逐个给叶子添加断言只会隐藏这条传播路径。

迁移完成的证据不是错误计数归零，而是目标配置确实生效，逃生口有清楚范围，关键边界有运行时测试。更细的分批方案属于渐进迁移主题；严格模式在这里负责定义最终检查基线。

<!-- /deep -->

[检查点: typescript/strict-mode](https://codewiki.com/zh/typescript/strict-mode/#checkpoint)

## 延伸阅读

- [TypeScript 配置参考：`strict`](https://www.typescriptlang.org/tsconfig/strict.html)
- [TypeScript 配置参考：`strictNullChecks`](https://www.typescriptlang.org/tsconfig/strictNullChecks.html)
- [TypeScript 配置参考：`strictFunctionTypes`](https://www.typescriptlang.org/tsconfig/strictFunctionTypes.html)
- [TypeScript 配置参考：`strictPropertyInitialization`](https://www.typescriptlang.org/tsconfig/strictPropertyInitialization.html)
- [TypeScript 配置参考：`useUnknownInCatchVariables`](https://www.typescriptlang.org/tsconfig/useUnknownInCatchVariables.html)
