严格模式指南
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* 选项 | false | 2.3 |
strictNullChecks | null 和 undefined 不再可赋值给其他类型 | false | 2.0 |
noImplicitAny | 禁止隐式推断为 any 类型 | false | 1.0 |
strictFunctionTypes | 函数参数使用逆变检查 | false | 2.6 |
strictBindCallApply | 检查 bind/call/apply 的参数类型 | false | 3.2 |
strictPropertyInitialization | 类属性必须在构造函数中初始化 | false | 2.7 |
noImplicitThis | 禁止隐式 any 类型的 this | false | 2.0 |
alwaysStrict | 在每个文件顶部注入 "use strict" | false | 2.1 |
useUnknownInCatchVariables | catch 变量类型为 unknown 而非 any | false | 4.4 |
noUncheckedIndexedAccess | 索引访问返回 T | undefined | false | 4.1 |
exactOptionalPropertyTypes | 区分 optional 属性与 undefined 值 | false | 4.4 |
noImplicitOverride | 重写父类方法必须用 override 关键字 | false | 4.3 |
注:浅蓝色行为 "strict": true 不包含的额外推荐标志。
strictNullChecks — null 安全检查
这是严格模式中最重要的标志。启用后,null 和 undefined 不再被视为所有类型的子类型。你必须通过类型收窄(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"); }
}
渐进式迁移指南
将一个大型项目从非严格模式迁移到完全严格模式可能令人生畏。以下是经过实践验证的逐步迁移策略:
- 先启用 noImplicitAny — 这是最基础的标志,能暴露类型推断薄弱的位置。通常产生的错误最多,但修复方式简单直接:添加类型注解即可。
- 然后启用 strictNullChecks — 这是价值最大的标志。修复方法包括添加 null 检查、使用可选链
?.和非空断言!。 - 逐个添加剩余标志 — 按照 strictPropertyInitialization、strictFunctionTypes、strictBindCallApply 的顺序依次启用。每次只加一个标志,修复所有错误后再加下一个。
- 使用
// @ts-expect-error临时抑制 — 对于暂时无法修复的复杂错误,可以用// @ts-expect-error标记,后续再回来处理。这比// @ts-ignore更好,因为当错误被修复后它会自动提示你移除注释。 - 最终切换到
"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,因为编译器无法保证索引处一定存在值。两者是互补的,建议一起启用。