严格模式指南

TypeScript 的 严格模式(strict mode)是一组编译器标志的集合,能在编译阶段捕获更多潜在的运行时错误。启用 "strict": true 后,TypeScript 会强制进行 null 检查、禁止隐式 any 类型、验证函数参数类型等。对于任何新项目,强烈建议从第一天就启用严格模式;对于已有项目,也可以通过逐步开启各个标志来渐进式迁移。

快速开始

tsconfig.json 中添加一行即可启用所有严格检查。"strict": true 是一个元标志,等同于同时启用下方表格中所有以 strict 开头的标志。

{
  "compilerOptions": {
    "strict": true,
    // Bonus flags (not included in "strict"):
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true
  }
}

所有严格模式标志一览

下表列出了 "strict": true 包含的标志以及推荐同时开启的额外标志。

标志名称作用默认值引入版本
strict元标志,启用以下所有 strict* 选项false2.3
strictNullChecksnull 和 undefined 不再可赋值给其他类型false2.0
noImplicitAny禁止隐式推断为 any 类型false1.0
strictFunctionTypes函数参数使用逆变检查false2.6
strictBindCallApply检查 bind/call/apply 的参数类型false3.2
strictPropertyInitialization类属性必须在构造函数中初始化false2.7
noImplicitThis禁止隐式 any 类型的 thisfalse2.0
alwaysStrict在每个文件顶部注入 "use strict"false2.1
useUnknownInCatchVariablescatch 变量类型为 unknown 而非 anyfalse4.4
noUncheckedIndexedAccess索引访问返回 T | undefinedfalse4.1
exactOptionalPropertyTypes区分 optional 属性与 undefined 值false4.4
noImplicitOverride重写父类方法必须用 override 关键字false4.3

注:浅蓝色行为 "strict": true 不包含的额外推荐标志。

strictNullChecks — null 安全检查

这是严格模式中最重要的标志。启用后,nullundefined 不再被视为所有类型的子类型。你必须通过类型收窄(narrowing)或可选链(?.)来安全地处理可能为空的值。这能在编译期捕获大量 "Cannot read property of null" 运行时错误。

❌ 错误写法
function greet(name: string | null) {
  // Error: Object is possibly 'null'.
  console.log(name.toUpperCase());
}

const el = document.getElementById("app");
// Error: Object is possibly 'null'.
el.innerHTML = "Hello";
✅ 正确写法
function greet(name: string | null) {
  // Optional chaining
  console.log(name?.toUpperCase());
  // Type narrowing
  if (name !== null) {
    console.log(name.toUpperCase());
  }
}

// Non-null assertion (use only when certain)
const el = document.getElementById("app")!;
el.innerHTML = "Hello";

noImplicitAny — 禁止隐式 any

当 TypeScript 无法推断出变量或参数的类型时,它会回退为 any。启用此标志后,这种隐式回退会产生编译错误。它迫使你为每个参数和变量提供明确的类型注解,从而大幅提高代码库的类型安全性。

❌ 错误写法
// Error: Parameter 'x' implicitly has an 'any' type.
function double(x) {
  return x * 2;
}

// Error: Parameter 'item' implicitly has an 'any' type.
const items = [1, 2, 3];
items.forEach(function (item, i) {
  // item is any here
});
✅ 正确写法
function double(x: number): number {
  return x * 2;
}

// Arrow functions infer from Array<number>
const items = [1, 2, 3];
items.forEach((item: number, i: number) => {
  console.log(item);
});

// Use unknown for truly dynamic values
function parse(input: string): unknown {
  return JSON.parse(input);
}

strictFunctionTypes — 函数参数逆变

此标志确保函数参数类型检查采用逆变(contravariant)规则,而非双变(bivariant)。简单来说,一个接受父类参数的函数不能赋值给接受子类参数的函数类型。这是类型系统更严谨的体现,能防止在回调函数中出现类型不匹配的运行时错误。

❌ 错误写法
type Handler = (e: MouseEvent) => void;

// Error: Type '(e: Event) => void' is not
// assignable to type 'Handler'.
const handler: Handler = (e: Event) => {
  // e.clientX would be undefined at runtime!
  console.log(e.type);
};
✅ 正确写法
type Handler = (e: MouseEvent) => void;

const handler: Handler = (e: MouseEvent) => {
  console.log(e.clientX); // safe
};

// Or use a broader type for the variable:
type AnyHandler = (e: Event) => void;
const general: AnyHandler = (e: Event) => {
  console.log(e.type);
};

strictPropertyInitialization — 类属性必须初始化

启用后,类的非可选属性必须在声明时赋默认值或在构造函数中初始化。这防止了访问未初始化属性导致的 undefined 错误。此标志要求同时启用 strictNullChecks

❌ 错误写法
class User {
  // Error: Property 'name' has no initializer
  // and is not definitely assigned.
  name: string;
  email: string;

  constructor() {
    // forgot to assign name and email!
  }
}
✅ 三种修复方式
class User {
  // 1. Initialize with default
  name: string = "";

  // 2. Assign in constructor
  email: string;
  constructor(email: string) {
    this.email = email;
  }
}

class Service {
  // 3. Definite assignment assertion
  // (use only when init happens externally)
  db!: Database;
  async init() { this.db = await connect(); }
}

useUnknownInCatchVariables — catch 中的 unknown

默认情况下,catch (e) 中的 e 类型是 any,这意味着你可以在不检查类型的情况下访问它的任何属性。启用此标志后,e 变为 unknown,强制你进行类型检查后再使用。因为在 JavaScript 中,throw 可以抛出任何值(不仅是 Error 对象),这个限制是完全合理的。

❌ 错误写法
try {
  JSON.parse(userInput);
} catch (e) {
  // e is 'unknown', cannot access .message directly
  console.log(e.message); // Error!
}
✅ 正确写法
try {
  JSON.parse(userInput);
} catch (e: unknown) {
  if (e instanceof Error) {
    console.log(e.message); // safe
  } else {
    console.log("Unknown error:", String(e));
  }
}

// Helper function for reuse:
function getErrorMessage(e: unknown): string {
  if (e instanceof Error) return e.message;
  return String(e);
}

noUncheckedIndexedAccess — 索引安全访问

此标志让通过索引访问数组元素或对象属性时,返回类型自动包含 undefined。例如 arr[0] 的类型不再是 T 而是 T | undefined。这可以防止越界访问数组或访问不存在的对象键导致的运行时错误。注意:此标志不包含在 "strict": true 中,需要单独启用。

❌ 错误写法
const arr = [1, 2, 3];
const val = arr[5]; // val: number | undefined
// Error: Object is possibly 'undefined'.
console.log(val.toFixed(2));

const map: Record<string, number> = { a: 1 };
const x = map["missing"]; // x: number | undefined
console.log(x.toFixed(2)); // Error!
✅ 正确写法
const arr = [1, 2, 3];
const val = arr[5];
if (val !== undefined) {
  console.log(val.toFixed(2)); // safe
}

// Nullish coalescing for defaults
const map: Record<string, number> = { a: 1 };
const x = map["missing"] ?? 0;
console.log(x.toFixed(2)); // safe, x is number

// Destructuring with default
const [first = 0] = arr;

noImplicitOverride — 显式 override 关键字

此标志要求在子类中重写父类方法时必须使用 override 关键字。这能防止因父类方法重命名而导致子类方法意外地变为普通方法(不再是重写),在重构大型继承层次结构时尤其有用。

❌ 错误写法
class Animal {
  move() { console.log("moving"); }
}

class Dog extends Animal {
  // Error: This member must have an 'override'
  // modifier because it overrides a member
  // in the base class 'Animal'.
  move() { console.log("running"); }
}
✅ 正确写法
class Animal {
  move() { console.log("moving"); }
}

class Dog extends Animal {
  override move() { console.log("running"); }
}

// Also catches typos:
class Cat extends Animal {
  // Error: This member cannot have an 'override'
  // modifier because it is not declared in the
  // base class 'Animal'.
  override moev() { console.log("sneaking"); }
}

渐进式迁移指南

将一个大型项目从非严格模式迁移到完全严格模式可能令人生畏。以下是经过实践验证的逐步迁移策略:

  1. 先启用 noImplicitAny — 这是最基础的标志,能暴露类型推断薄弱的位置。通常产生的错误最多,但修复方式简单直接:添加类型注解即可。
  2. 然后启用 strictNullChecks — 这是价值最大的标志。修复方法包括添加 null 检查、使用可选链 ?. 和非空断言 !
  3. 逐个添加剩余标志 — 按照 strictPropertyInitialization、strictFunctionTypes、strictBindCallApply 的顺序依次启用。每次只加一个标志,修复所有错误后再加下一个。
  4. 使用 // @ts-expect-error 临时抑制 — 对于暂时无法修复的复杂错误,可以用 // @ts-expect-error 标记,后续再回来处理。这比 // @ts-ignore 更好,因为当错误被修复后它会自动提示你移除注释。
  5. 最终切换到 "strict": true — 当所有个别标志都启用后,替换为 "strict": true 以自动包含未来版本新增的严格标志。

逐文件启用严格检查

对于 JavaScript 文件可以使用 // @ts-check 来单独启用类型检查:

// @ts-check

/** @type {string} */
let name = "Alice";

// Error: Type 'number' is not assignable to type 'string'.
name = 42;

常见问题 (FAQ)

新项目应该启用严格模式吗?

是的,强烈建议。从第一天启用 "strict": true 几乎没有额外成本,因为新代码会自然地按照严格规则编写。TypeScript 团队和社区一致推荐新项目默认开启严格模式,所有主流框架(React、Angular、Vue、Next.js)的项目模板也默认启用。

如何修复 "Object is possibly null" 错误?

有四种常用方法:

1. 类型收窄 — 使用 if (obj !== null) 检查后再使用
2. 可选链 — 使用 obj?.property 安全访问
3. 空值合并 — 使用 obj ?? defaultValue 提供默认值
4. 非空断言 — 使用 obj!,但仅在你确定值不为 null 时使用

"strict": true 和单独启用各个标志有什么区别?

"strict": true 是一个元标志,相当于同时启用所有以 strict 开头的标志加上 noImplicitAny、noImplicitThis、alwaysStrict 和 useUnknownInCatchVariables。关键区别在于:当未来 TypeScript 版本新增严格标志时,"strict": true 会自动包含这些新标志,而单独启用的方式不会。因此,最终目标应该是使用 "strict": true

可以渐进式启用严格模式吗?

当然可以。推荐的做法是从 noImplicitAny 开始,然后逐个添加标志,每次修复完所有错误再添加下一个。对于超大型代码库,还可以考虑使用 typescript-strict-plugin 等工具来逐目录开启严格模式。整个过程可能需要数周甚至数月,但每一步都在提升代码质量。

strictNullChecks 和 noUncheckedIndexedAccess 有什么关系?

strictNullChecks 确保显式声明为 T | null 的类型必须进行 null 检查,但它不会影响索引访问的返回类型。noUncheckedIndexedAccess 更进一步,让 arr[i]obj[key] 的返回类型自动包含 undefined,因为编译器无法保证索引处一定存在值。两者是互补的,建议一起启用。

速查搜索