TypeScript 类型收窄有哪些方式?
简化版
类型收窄是把宽类型缩小到更具体类型。常见方式有 typeof、instanceof、in、判别联合、真值判断、自定义类型守卫和控制流分析。
详细版
示例:
function print(value: string | number) {
if (typeof value === 'string') {
value.toUpperCase();
} else {
value.toFixed();
}
}
判别联合:
type Result =
| { type: 'success'; data: string }
| { type: 'error'; message: string };
function handle(r: Result) {
if (r.type === 'success') {
console.log(r.data);
}
}
自定义类型守卫:
function isString(x: unknown): x is string {
return typeof x === 'string';
}
完整版教学
一、为什么需要类型收窄
联合类型表示一个值可能是多种类型。使用它之前,TS 要你证明当前到底是哪一种。
这不是 TS 麻烦,而是避免运行时错误。例如 string | number 不能直接调用 toUpperCase,因为 number 没这个方法。
二、typeof 和 instanceof
typeof 适合原始类型:
typeof x === 'boolean'
instanceof 适合类实例:
if (err instanceof Error) {
console.log(err.message);
}
三、in 操作符
当多个对象类型有不同属性时,可以用 in:
if ('swim' in animal) {
animal.swim();
}
它通过属性存在性缩小类型范围。
四、判别联合是工程最佳实践
给每个分支加一个稳定的字面量字段,比如 type、kind,TS 就能非常准确地收窄。
这在接口状态、弹窗类型、图形类型、消息类型中特别常用。相比猜测某个可选属性是否存在,稳定判别字段能让每条控制流分支对应唯一成员。
五、面试追问与工程落地
类型收窄常见追问是“自定义类型守卫是不是一定安全”。类型守卫的返回类型 x is User 是你对编译器的承诺,TS 不会验证函数内部逻辑是否真的完整。如果守卫写错,后面仍可能运行时报错。
还会问可选链是不是类型收窄。user?.name 可以安全访问,但不一定在后续代码里把 user 收窄成非空。真正需要后续复用非空对象时,还是写明确判断:
if (!user) return;
user.name;
工程里建议对接口响应使用运行时 schema 校验,如 Zod、Valibot 或自写 guard,再把 unknown 收窄成业务类型。这样 TypeScript 的静态类型和运行时真实数据才能对齐。
六、理解控制流分析何时保留或撤销收窄
TypeScript 会沿控制流记录赋值、返回、抛错和条件判断,因此提前返回后,后续分支可以自动排除已经处理的类型。收窄并非给变量永久改类型:变量重新赋值、对象属性可能被修改,或者闭包延迟执行时,编译器可能撤销先前结论。稳定的局部常量通常比跨回调依赖可变属性更容易保持安全。
function format(value: string | number | null) {
if (value === null) return 'empty'
if (typeof value === 'number') return value.toFixed(2)
return value.toUpperCase()
}
// 3 个候选经过 null 判断剩 2 个,再经 typeof 各剩 1 个
| 守卫 | 适合对象 | 关键边界 |
|---|---|---|
typeof | 原始类型、函数 | typeof null === 'object' |
instanceof | 有真实构造器的实例 | 跨 realm 或伪造原型需谨慎 |
'key' in obj | 按属性存在性区分对象 | 可选属性可能出现在两边 |
| 真值判断 | 快速排除假值 | 会同时排除 ''、0、NaN |
| 判别字段 | 业务状态联合 | 字段必须稳定且分支值唯一 |
如果 string | null 用 if (value) 收窄,空字符串也会进入 false 分支;这不是编译器错误,而是真值语义本来就包含多个假值。需要只排除空值时,应写 value != null 或分别比较 null、undefined。
类型守卫是一份证明:先确认它在运行时检查了哪些事实,再看这些事实是否足够推出声明的目标类型。
七、常见误区与追问
- 误区:typeof null 的结果是 null。 JavaScript 运行时结果是
'object',所以对象判断还必须排除 null。 - 误区:真值判断只会排除 null 和 undefined。 它还会排除空字符串、0、NaN、false 等假值,可能误伤合法业务数据。
- 误区:自定义 x is T 守卫一定可信。 返回类型是开发者对编译器的承诺,函数体检查不完整仍会造成运行时错误。
- 追问:为什么提前 return 能帮助收窄? 控制流分析知道被排除类型无法到达后续路径,因此从剩余联合中移除它。
- 追问:in 的 false 分支一定没有该属性吗? 不一定;带可选属性的类型可能同时保留在 true 与 false 两边,需要结合模型设计判断。
- 追问:重新赋值后为什么类型变宽? 新赋值改变了当前控制流事实,编译器必须按声明类型和新值重新分析。
- 追问:如何让复杂状态更容易收窄? 使用稳定的字面量判别字段,并在 default 分支做 never 穷尽检查。
八、加强记忆
类型收窄就是“先验明身份,再调用能力”。基础类型用 typeof,类实例用 instanceof,对象属性用 in,复杂状态优先设计判别联合。
再检查守卫是否误排除了合法假值、是否会被重新赋值撤销,收窄结论才经得住边界追问。