主题
类型缩小(Type Narrowing)
什么是「类型缩小」?
TypeScript 的控制流分析会在当前作用域内根据条件判断把某个值的类型从宽(例如 T | undefined)缩小为更具体的类型(例如 T)。但这种缩小只在当前的控制流/函数作用域内可靠生效,跨函数、跨 await/异步边界或闭包时通常不起作用。
为什么会失效
- 函数边界:在 if 块外定义/调用的函数无法“记住”外层的缩小状态。
- await/异步:await 表明中间可能会有外部变更,编译器不会继续假设此前的缩小成立。
- 可变变量(let/var):非 const 的局部变量可能被修改,缩小不稳定。
- 闭包/回调:编译器不能保证回调执行时变量仍然满足之前的条件。
「类型缩小」示例
错误示例(不可依赖跨函数缩小):
ts
type Props = { obj?: { name: string; id: number } }
const props: Props = {}
if (props.obj) {
foo()
}
function foo() {
console.log(props.obj.name) // Error: 可能为 undefined
}await 导致缩小失效:
ts
type Props = { obj?: { name: string; id: number } }
const props: Props = {}
if (props.obj) {
await someAsync()
console.log(props.obj.name) // Error: 可能为 undefined
}解决方案(按推荐顺序)
- 局部 const 拷贝并传参(推荐)
ts
if (props.obj) {
const obj = props.obj // 局部 const 已被缩小
foo(obj)
}
function foo(o: { name: string; id: number }) {
console.log(o.name)
}- 早返回(在同一函数内)
ts
function doIt() {
if (!props.obj) return
// 这里 props.obj 已缩小
console.log(props.obj.name) // OK
}- 用户自定义类型守卫(type guard)
ts
function hasObj(p: Props): p is Props & { obj: { name: string; id: number } } {
return !!p.obj
}
if (hasObj(props)) {
console.log(props.obj.name) // 在 if 块内被缩小
}- 断言函数(asserts) — 在通过校验后,对后续代码生效
ts
function assertHasObj(p: Props): asserts p is Props & { obj: { name: string } } {
if (!p.obj) throw new Error('missing obj')
}
assertHasObj(props)
console.log(props.obj.name) // 在 assert 调用之后被视为存在- 可选链(安全读取,返回 undefined)
ts
console.log(props.obj?.name) // 如果 obj 为 undefined,结果为 undefined- 非空断言(!)——不安全但简便
ts
console.log(props.obj!.name) // 告诉编译器“我确定存在”,若实际为 undefined 会抛运行时错误- 把值复制到局部并在 await 之后使用
ts
if (props.obj) {
const obj = props.obj
await someAsync()
console.log(obj.name) // 安全:使用的是之前的局部常量
}其它有用的缩小手段
- typeof / instanceof / in
ts
if (typeof x === 'string') {
/* x: string */
}
if (obj instanceof Date) {
/* obj: Date */
}
if ('prop' in obj) {
/* obj.prop 存在 */
}- 判别联合(discriminated unions)
ts
type A = { kind: 'a'; foo: string }
type B = { kind: 'b'; bar: number }
type U = A | B
function f(u: U) {
if (u.kind === 'a') {
console.log(u.foo) // u 被缩小为 A
}
}