TypeScript 类型收窄技巧:让联合类型替你挡住错误

TypeScript 的价值不只是给变量标注类型,而是把“不可能状态”尽量排除在编译阶段。类型收窄能让一份联合类型随着控制流逐步变具体,减少强制断言和运行时分支遗漏。

用可辨识联合表达状态

不要用多个可选字段表达互斥状态:

type RequestState<T> =
  | {status: 'idle'}
  | {status: 'loading'}
  | {status: 'success'; data: T}
  | {status: 'error'; message: string}

function render<T>(state: RequestState<T>) {
  switch (state.status) {
    case 'success':
      return state.data
    case 'error':
      return state.message
    default:
      return null
  }
}

这样 success 状态一定有 dataerror 状态一定有 message,不用到处写可选链。

把外部输入当作 unknown

API、LocalStorage、PostMessage 和第三方 SDK 返回的数据都不可信。any 会关闭检查,unknown 会迫使调用方验证:

function isUser(value: unknown): value is User {
  if (!value || typeof value !== 'object') return false
  const record = value as Record<string, unknown>
  return typeof record.id === 'string' && typeof record.name === 'string'
}

大型项目可用 Schema 校验库在运行时解析,并从 Schema 推导 TypeScript 类型,避免“类型声明正确、真实 JSON 错误”。

善用内置收窄

  • typeof:适合基本类型。
  • instanceof:适合同一运行时中的类实例。
  • in:按属性区分对象联合。
  • 相等判断:让两个变量相互收窄。
  • 自定义 Type Predicate:复用领域校验。

注意 typeof null === 'object',也不要用真值判断把空字符串或数字 0 当成缺失值。

用 never 检查分支完整性

function assertNever(value: never): never {
  throw new Error(`Unhandled state: ${JSON.stringify(value)}`)
}

function color(status: RequestState<unknown>['status']) {
  switch (status) {
    case 'idle': return 'gray'
    case 'loading': return 'blue'
    case 'success': return 'green'
    case 'error': return 'red'
    default: return assertNever(status)
  }
}

未来新增状态但忘记更新这里,编译器会直接报错。这比一个静默的 default 更安全。

satisfies 比粗暴断言更可靠

as SomeType 可能掩盖错误,satisfies 会检查结构,同时保留对象字面量的精确类型:

const routes = {
  home: '/',
  article: '/articles',
} satisfies Record<string, `/${string}`>

实战规则

  1. 领域状态优先使用可辨识联合,不堆布尔变量。
  2. 系统边界统一接收 unknown,校验后再进入业务层。
  3. 尽量消除 as 和非空断言 !,每个断言都应能解释依据。
  4. 公共函数返回明确联合,让调用方处理失败路径。
  5. Switch 对封闭状态使用 never 做穷尽检查。

类型设计不是追求复杂技巧,而是让错误状态难以构造,让正确用法成为最省事的用法。

参考资料