为什么收窄这么重要
类型收窄(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 |
关键原则:让类型信息顺着控制流走,而不是靠程序员手动声明。