OctoBlog 以 GitHub 为蓝本的个人博客系统
全部文章

TypeScript 类型收窄的 7 个技巧

为什么收窄这么重要

类型收窄(narrowing)解决的是同一个问题:我知道这里是什么,编译器不知道。

写得好,业务代码里几乎不需要 as;写得差,就会出现满屏的 as any 和 // @ts-ignore。

1. typeof 只对基础类型有效

TS
function format(value: string | number | boolean) {
  if (typeof value === 'boolean') return value ? '是' : '否';
  if (typeof value === 'number') return value.toFixed(2);
  return value.trim();
}

注意:typeof null === 'object',所以 typeof x === 'object' 不够,要额外判 x !== null。

2. 判真值要小心 0 和 ''

TS
// 危险:id 为 0 时会被当成"没有 id"
if (user.id) { /* ... */ }

// 正确
if (user.id != null) { /* ... */ }

3. Array.isArray 是数组的唯一正确判据

TS
type Payload = string | string[];
function first(payload: Payload) {
  return Array.isArray(payload) ? payload[0] : payload;
}

4. 可辨识联合(discriminated union)是主力

TS
type Result =
  | { status: 'loading' }
  | { status: 'ok'; data: Article[] }
  | { status: 'error'; message: string };

function render(result: Result) {
  switch (result.status) {
    case 'loading': return '加载中…';
    case 'ok': return result.data.map((a) => a.title).join('、');   // data 在这里存在
    case 'error': return `出错:${result.message}`;
  }
}

只要 switch 覆盖全部 status 且没有 default,TS 还能帮你做穷尽性检查。

5. in 操作符适合结构不同的对象

TS
type A = { kind: 'a'; a: number };
type B = { kind: 'b'; b: string };

function pick(x: A | B) {
  if ('a' in x) return x.a;   // 收窄为 A
  return x.b.length;
}

6. 断言函数(assertion function)用于校验器

TS
function assertDefined<T>(value: T | null | undefined, name: string): asserts value is T {
  if (value == null) throw new Error(`${name} is required`);
}

function handler(input: string | undefined) {
  assertDefined(input, 'input');
  return input.toUpperCase();   // 此后 input: string
}

这是我写得最多的一类工具函数——把校验和收窄合并成一步。

7. satisfies 让配置既有校验又有字面量类型

TS
const config = {
  retry: 3,
  timeout: 5000,
} satisfies Record<string, number>;

config.retry;   // 类型是 3,而不是 number

小结

场景 首选手段
基础类型分支 typeof
空值检查 != null
数组 / 元组 Array.isArray
状态机 可辨识联合 + switch
结构差异 in
前置校验 断言函数
配置对象 satisfies

关键原则:让类型信息顺着控制流走,而不是靠程序员手动声明。