先记住这个答案
类型收窄是 TypeScript 根据检查、赋值和控制流,把某个位置上可能出现的类型范围缩小。一个变量声明为字符串或数字,经过 typeof 判断后,分支内可以只剩字符串;如果数字分支提前返回,后续路径也能排除数字。声明类型仍决定未来允许赋入什么,当前位置的观察类型则决定现在能安全执行什么操作。收窄不会把值转换成另一种类型,也不代表编译器能够追踪任意跨函数副作用或永久保证共享对象没有被修改。
- 收窄依据当前位置可达路径上的证据
- 重新赋值仍按照变量声明的契约检查
- 类型范围变化不会执行运行时值转换
提前返回如何影响后面的类型
条件判断不必始终配套 else 才能建立证据。如果某一种候选在前面已经返回,继续运行到后面就说明当前值不属于那个候选。控制流分析结合可达性处理这种关系,使校验式的早返回代码同样能获得明确的类型提示。
示例先处理数字,其余路径只剩字符串。另一个函数展示赋值后的观察类型会变化,但允许赋入的集合仍是原来的联合。布尔值不属于声明契约,所以即使变量当前已经收窄成字符串,也不能借此接收任意新类型。
function display(value: string | number): string {
if (typeof value === 'number') return value.toFixed(2);
return value.trim();
}
function change(value: string | number) {
value = 12;
const numeric: number = value;
value = 'ready';
const textual: string = value;
// @ts-expect-error boolean 不属于声明的联合
value = true;
return { numeric, textual };
}数字路径退出后,display 的后续语句可调用字符串方法。change 中的两次赋值产生不同观察类型,预期错误则验证声明范围没有随着局部收窄被任意放宽。
合流位置为什么又看见联合
如果两个分支分别给变量赋字符串和数字,离开分支后两条路径都可能到达,因此当前位置可能重新出现两者的联合。这样的合流是对运行路径的保守描述,而不是编译器忘记了刚才的判断。需要某一种能力时,可以继续检查或分别在分支内完成操作。
某些别名条件和解构后的关联能够被现代 TypeScript 分析,但支持有具体条件,例如绑定是否稳定以及是否继续赋值。排查时优先查看出错位置的实际类型,简化到最小代码,再判断是模型丢失关联还是编译器没有追踪该模式。
闭包与共享可变字段要谨慎
回调可能在稍后执行,期间对象属性或变量可能被其他代码修改,因此不能假定所有外层检查都会原样保留到任意闭包。需要稳定原始值时,可以在已收窄位置保存局部常量,再让回调使用该值;需要最新值则应在使用时重新检查。
同时,编译器也不会完整模拟所有函数副作用,有些共享属性变化可能没有让已有收窄失效。类型通过不是并发或可变性证明。应限制共享写入、明确所有权,并用运行时场景验证先检查后使用之间是否可能发生变化。
容易答错的地方
- 收窄后变量以后只能接收这个窄类型
- 赋值仍受原声明类型约束,新的合法赋值可以改变当前位置的观察类型;需要区分长期契约与某条路径上的当前事实。
- 编译器会自动把联合值转换成目标类型
- 收窄分析不会生成字符串转数字之类的代码,实际值仍然是原值;需要转换时应显式执行并检查转换失败情况。
面试官还会怎么问?
if 分支外为什么不能继续使用专有字段?
若分支没有排除其他路径,离开后仍可能收到其余候选;应在分支内使用字段,或让不满足条件的路径提前结束。
保存局部常量就能冻结整个对象吗?
不能,常量只稳定绑定,引用对象仍可能被修改;保存原始字段快照与保存整个对象引用拥有不同的稳定性保证。
所有返回 boolean 的函数都会收窄参数吗?
不能一概而论,编译器可能推断特定形式的类型谓词,也支持显式谓词;普通任意布尔计算并不自动表达调用方参数的完整类型关系。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。