如何让TypeScript将对象的type属性作为判别式实现类型收窄?
type属性收窄泛型对象的类型? 问题描述
在以下TypeScript代码中,当判断obj.type === "Bar"时,TypeScript无法将obj.relatedObject的类型收窄为ObjTwo,报错提示属性three不存在于ObjOne | ObjTwo类型上:
type Types = "Foo" | "Bar"; interface ObjOne { one: "one"; two: "two"; } interface ObjTwo { three: "three"; four: "four"; } type ExtractRelatedObject<T extends Types> = T extends "Foo" ? ObjOne : T extends "Bar" ? ObjTwo : never; interface Baz<T extends Types> { type: T; relatedObject: ExtractRelatedObject<T>; } function foo<T extends Types>(obj: Baz<T>) { if (obj.type === "Bar") { obj.relatedObject.three; //^?? Property 'three' does not exist on type 'ObjOne | ObjTwo'. } }
原因分析
这是因为泛型参数T是一个单一的、未具体化的类型变量,而不是联合类型。当你在foo函数中使用Baz<T>时,TypeScript无法将T拆解为联合类型的成员来进行分布式条件类型的匹配。即使T的约束是Types(即"Foo" | "Bar"),但在函数内部,T被视为一个可能是"Foo"、"Bar"或整个联合类型的单一类型,此时ExtractRelatedObject<T>会被解析为ObjOne | ObjTwo,而非根据type属性动态关联的具体类型。
简言之:泛型函数中的类型参数是“整体”处理的,不会自动展开为联合类型进行分布式收窄。
修复方法及验证
有效修复:使用联合类型替代泛型参数
将函数参数类型改为Baz<"Foo"> | Baz<"Bar">,此时TypeScript会将联合类型的每个成员单独处理,通过type属性的判断可以正确收窄relatedObject的类型:
function fooFix1(obj: Baz<"Foo"> | Baz<"Bar">) { if (obj.type === "Bar") { obj.relatedObject.three; // ^?? 正确识别为ObjTwo.three: "three" } }
这种写法的本质是让TypeScript直接处理已具体化的联合类型,而非未绑定的泛型变量,此时类型收窄可以正常工作。
无效尝试的原因分析
为泛型参数添加默认类型Types的写法无效,因为默认类型只是在调用函数未指定T时的 fallback,函数内部的T仍然是一个单一的类型变量,不会自动展开为联合类型:
function fooBroken1<T extends Types = Types>(obj: Baz<T>) { if (obj.type === "Bar") { obj.relatedObject.three; //^?? 仍然报错:Property 'three' does not exist on type 'ObjOne | ObjTwo'. } }
这里的T依然是一个泛型变量,TypeScript无法确定它是"Foo"、"Bar"还是整个联合类型,因此无法进行精准的类型收窄。
内容的提问来源于stack exchange,提问作者Magnus

