正在加载,请稍候…

TypeScript 严格模式:功能详解与零破坏迁移指南

完整指南:TypeScript 严格模式各选项含义、增量迁移策略、满足类型检查的常见模式,以及严格模式如何捕获真实 bug。

TypeScript Strict Mode: What It Does and How to Enable It Without Breaking Every

为什么严格模式很重要

没有严格模式的 TypeScript 只是带有可选类型注解的 JavaScript。而启用严格模式的 TypeScript 则是一种不同的语言,它能在编译时捕获一整类 bug。

两者之间的差距比大多数开发者意识到的要大。当 strictNullChecks: false 时,nullundefined 是所有类型的子类型——这意味着类型检查器会愉快地接受那些在运行时因“Cannot read properties of undefined”而崩溃的代码。这是最常见的 JavaScript 运行时错误,而且是可以预防的。

TypeScript Strict Mode: What It Does and How to Enable It Without Breaking Every illustration

“strict”实际启用了什么

strict: true 是 tsconfig.json 中启用多个标志的简写:

{
  "compilerOptions": {
    // 以下全部由 "strict: true" 启用:
    "strictNullChecks": true,         // null/undefined 是独立的类型
    "noImplicitAny": true,            // 不能使用隐式 any
    "strictFunctionTypes": true,      // 函数参数类型逆变检查
    "strictBindCallApply": true,      // bind/call/apply 具有正确的类型
    "strictPropertyInitialization": true,  // 类属性必须初始化
    "noImplicitThis": true,           // 'this' 必须有显式类型
    "alwaysStrict": true,             // 在 JS 输出中生成 "use strict"
    "useUnknownInCatchVariables": true // catch 变量为 'unknown' 而非 'any'
  }
}

另外一些有价值的选项(不在 strict 捆绑中):

{
  "compilerOptions": {
    "noUncheckedIndexedAccess": true,    // array[n] 可能为 undefined
    "exactOptionalPropertyTypes": true,  // {a?: string} ≠ {a: string | undefined}
    "noImplicitReturns": true,           // 所有代码路径都必须返回
    "noFallthroughCasesInSwitch": true,  // 禁止 switch 意外穿透
    "noImplicitOverride": true,          // 类覆盖需要 'override' 关键字
  }
}

strictNullChecks:最重要的一个

// 没有 strictNullChecks:
function getLength(s: string) {
  return s.length  // 没问题……但 s 可能是 null!
}
getLength(null)    // TypeScript 说 ✅,运行时说 ❌

// 启用 strictNullChecks:
function getLength(s: string) {
  return s.length
}
getLength(null)   // ❌ TypeScript 错误:null 不能赋值给 string

// 现在你必须处理 null 的情况:
function getLength(s: string | null): number {
  if (s === null) return 0
  return s.length
}

// 或者使用可选链和空值合并:
const length = s?.length ?? 0

TypeScript Strict Mode: What It Does and How to Enable It Without Breaking Every illustration

使用 strictNullChecks 进行类型收窄

interface User {
  id: number
  name: string
  address?: {    // 可选——可能为 undefined
    city: string
    country: string
  }
}

function displayUser(user: User | null) {
  // ❌ 没有收窄:
  console.log(user.name)           // 错误:user 可能为 null
  console.log(user.address.city)   // 错误:user.address 可能为 undefined
  
  // ✅ 正确收窄:
  if (!user) return
  
  console.log(user.name)           // ✅ 此处 user 为 User
  
  // address 仍然是可选的
  console.log(user.address?.city)  // ✅ 可选链:string | undefined
  console.log(user.address?.city ?? 'Unknown')  // ✅ 非空:string
  
  // 通过解构收窄:
  const { address } = user
  if (address) {
    console.log(address.city)  // ✅ 此处 address 为 { city: string, country: string }
  }
}

noImplicitAny:不再隐藏类型错误

// ❌ 没有 noImplicitAny——以下代码编译通过:
function processData(data) {  // data: any(隐式)
  return data.someProperty.nested.value  // 没有类型检查!
}

// ✅ 启用 noImplicitAny——必须显式声明类型:
function processData(data: ApiResponse) {  // 必须显式
  return data.result.items[0].name        // 类型检查通过!
}

// 当你确实不知道类型时(外部数据):
function processUnknown(data: unknown) {
  // unknown 强制你在使用前进行收窄
  if (typeof data === 'object' && data !== null && 'name' in data) {
    console.log((data as { name: string }).name)
  }
}

// 对于逃生舱口,要显式声明:
const value = JSON.parse(rawJson) as ApiResponse  // 显式转换
// 或者使用 zod 进行运行时验证:
const result = ApiResponseSchema.parse(JSON.parse(rawJson))

TypeScript Strict Mode: What It Does and How to Enable It Without Breaking Every illustration

strictPropertyInitialization

// ❌ 没有 strict——编译通过但运行时崩溃:
class UserService {
  db: Database           // 声明但从未赋值!
  
  findUser(id: number) {
    return this.db.query(id)  // RuntimeError: cannot read 'query' of undefined
  }
}

// ✅ 启用 strict——三种修复方式:

// 选项 1:在构造函数中初始化
class UserService {
  db: Database
  
  constructor(db: Database) {
    this.db = db    // 在构造函数中赋值
  }
}

// 选项 2:内联初始化
class UserService {
  db: Database = new MockDatabase()
}

// 选项 3:确定赋值断言(当你确定在其他地方已初始化时)
class UserService {
  db!: Database  // '!' 告诉 TypeScript “我知道已赋值,相信我”
  
  initialize(db: Database) {
    this.db = db
  }
}

noUncheckedIndexedAccess:数组安全

// 没有 noUncheckedIndexedAccess:
const arr = [1, 2, 3]
const first: number = arr[0]  // TypeScript 说 number,但可能为 undefined!

// 启用 noUncheckedIndexedAccess:
const arr = [1, 2, 3]
const first = arr[0]         // 类型:number | undefined

// 现在你必须处理 undefined 的情况:
if (first !== undefined) {
  console.log(first * 2)
}

// 或者断言非空(谨慎使用):
const first = arr[0]!        // 断言非空

// 更好的做法:使用安全访问模式
const first = arr.at(0)      // 返回 T | undefined——与启用 noUncheckedIndexedAccess 的 arr[0] 相同
const sum = arr.reduce((acc, n) => acc + n, 0)  // 无需索引访问

// 对象索引签名:
interface Config {
  [key: string]: string
}

const config: Config = { debug: 'true' }
const value = config['debug']  // 启用 noUncheckedIndexedAccess:string | undefined

增量迁移到严格模式

不要试图在大型现有代码库中一次性启用严格模式——错误数量会让人不知所措。请逐步启用标志:

// tsconfig.json——从影响最大的开始
{
  "compilerOptions": {
    // 阶段 1:从这里开始(最容易见效)
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    
    // 阶段 2:最重要的一个
    "strictNullChecks": true,
    
    // 阶段 3:额外的严格性
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "useUnknownInCatchVariables": true,
    
    // 阶段 4:最大严格性
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}

技巧:使用带跟踪注释的 ts-ignore 抑制

// 当无法立即修复错误时,标记为待办:
// @ts-expect-error TODO(2026-07-01): 在 UserService 重构后修复
const value = legacyUserService.getUnsafeData()

// ts-expect-error 优于 ts-ignore:
// - 如果错误消失,ts-expect-error 会报错(强制你移除它)
// - ts-ignore 即使错误已修复也会静默保留

启用严格后的常见模式

// 模式 1:类型守卫
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    typeof (value as any).id === 'number' &&
    'name' in value &&
    typeof (value as any).name === 'string'
  )
}

// 模式 2:断言函数
function assertDefined<T>(value: T | null | undefined, message?: string): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error(message ?? 'Expected value to be defined')
  }
}

const user = getUser(id)  // User | null
assertDefined(user, 'User not found')
console.log(user.name)    // ✅ TypeScript 知道此处 user 是 User

// 模式 3:穷举 switch
类型 Status = 'pending' | 'active' | 'inactive'

function displayStatus(status: Status): string {
  switch (status) {
    case 'pending': return 'Awaiting approval'
    case 'active': return 'Active'
    case 'inactive': return 'Inactive'
    default:
      // 如果你添加新的 Status 值,这里会变成类型错误
      const _exhaustive: never = status
      throw new Error(`Unhandled status: ${_exhaustive}`)
  }
}

// 模式 4:可辨识联合类型替代 any
type ApiResult<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; error: string; code: number }
  | { status: 'loading' }

function handleResult<T>(result: ApiResult<T>) {
  switch (result.status) {
    case 'success':
      console.log(result.data)   // ✅ TypeScript 知道此处 data 存在
      break
    case 'error':
      console.log(result.error, result.code)  // ✅ TypeScript 知道 error/code 存在
      break
    case 'loading':
      console.log('Loading...')  // 无法访问 data 或 error
      break
  }
}

严格模式 TypeScript 是前端代码库中投资回报率最高的项目之一。修复类型错误的前期成本是真实但有限的。而持续的好处——消除生产环境中的“Cannot read properties of undefined”——会无限累积。

→ 使用 Text Statistics 工具分析文本内容统计(字数、字符数)。