TypeScript联合类型窄化行为版本差异及类型推断疑问
先看你提到的测试代码:
type Name = { name: string } type Age = { age: number } type UnionBoth = Name | Age const test = (union: UnionBoth): void => { if ('name' in union && 'age' in union) { union.name union.age } }
你发现TypeScript 5.4.3中这段代码无报错,但4.8.4版本会提示Property 'name' does not exist on type 'never',同时还有关于类型推断和心智模型的疑问,以下是解答:
一、版本差异的原因
旧版本(如4.8.4)的TypeScript对联合类型的窄化逻辑存在局限:它认为Name | Age的联合成员只能是“仅Name”或“仅Age”,默认排除了“同时属于两者”的可能性。因此当你用'name' in union && 'age' in union做判断时,TypeScript会觉得没有任何值能满足这个条件,所以把代码块内的union推断为never类型,自然报错。
而新版本(5.4.3)改进了联合类型的窄化逻辑,能够正确识别联合类型的成员可以存在交叉(即某个值同时属于多个联合成员的类型),因此会将代码块内的union推断为同时包含name和age的类型,允许访问这两个属性。
二、正确的心智模型
联合类型A | B的本质是:所有属于A、属于B,或者同时属于A和B的值的集合。
用in操作符进行类型窄化时,TypeScript会逐步过滤联合类型的成员:
- 当判断
'name' in union时,会把联合类型过滤为Name | (Age & { name: unknown })——因为Age本身没有name,但可能存在某个值同时符合Age和带有name的类型; - 接着判断
'age' in union,会进一步过滤出同时拥有name和age的类型,最终得到既包含Name的明确属性,又包含未知类型age的交叉类型。
三、为什么推断类型是Name & Record<"age", unknown>
Name类型明确只有name: string属性,而我们通过'age' in union仅能确认age属性存在,但无法确定它的具体类型——毕竟Name | Age的联合类型中,除了Name & Age(即{ name: string, age: number }),还可能存在比如{ name: string, age: boolean }这类同时符合Name和带有任意类型age的对象。
TypeScript用Record<"age", unknown>来表示“存在age属性,但其类型未知”,和Name交叉后,就得到了既有明确name: string、又有未知类型age的类型。
四、你的心智模型是否正确?
完全正确!你的模型精准描述了联合类型的集合本质:联合类型包含“仅Name”“仅Age”“Name & Age”三个子集,通过in操作符的连续判断,确实可以窄化到“同时属于两个类型的交集”这个场景,新版本的TypeScript已经能正确适配这种逻辑。
内容的提问来源于stack exchange,提问作者NeoZoom.lua

