TypeScript条件类型结合联合类型映射的异常行为及解决问询
先定义联合类型Entities:
export type CompErrors = { type: "compilation_errors" val: number } export type TestErrors = { type: "test_errors" } export type TestInfo = { type: 'test_info' } export type Entities = CompErrors | TestErrors | TestInfo
创建EntityMap类型将联合转为以type为键的对象类型,此时函数参数要求必填所有键:
type EntityMap<Type> = { [T in Extract<Type, { type: string }> as T["type"]]: ((par: T) => any) | string | boolean | object } type Params = EntityMap<Entities> const entityMapFn = (params: Params) => {} // 缺少test_info会报错 entityMapFn({ compilation_errors: "", test_errors: "", })
但添加布尔条件分支后,即便分支未触发,生成的类型不再要求必填所有属性:
type EntityMapAlternative<Type> = Type extends boolean ? string : { [T in Extract<Type, { type: string }> as T["type"]]: ((par: T) => any) | string | boolean | object } type ParamsAlternative = EntityMapAlternative<Entities> const entityMapAlternativeFn = (params: ParamsAlternative) => {} // 缺少test_info无报错 entityMapAlternativeFn({ compilation_errors: "", test_errors: (te) => {te.type}, })
提取映射类型后结果仍一致:
type EntityMapExtracted<Type> = Type extends boolean ? string : EntityMap<Type> type ParamsExtracted = EntityMapExtracted<Entities> const entityMapExtractedFn = (params: ParamsExtracted) => {} // 缺少test_info无报错 entityMapExtractedFn({ compilation_errors: "", test_errors: (te) => {te.type}, })
疑问:为何布尔分支未触发却改变了类型结果?如何实现预期的必填所有属性的效果?
原因:条件类型的分布式特性
当条件类型的泛型参数是联合类型时,TypeScript会自动触发分布式条件类型——它会把联合类型拆成单个成员,分别代入条件类型计算,最后把结果重新合并成联合类型。
在这个例子里,Entities是CompErrors | TestErrors | TestInfo,代入Type extends boolean ? ... : ...时,会拆成三次独立计算:
CompErrors extends boolean ? string : EntityMap<CompErrors>→ 得到{ compilation_errors: ... }TestErrors extends boolean ? string : EntityMap<TestErrors>→ 得到{ test_errors: ... }TestInfo extends boolean ? string : EntityMap<TestInfo>→ 得到{ test_info: ... }
最终ParamsAlternative是这三个单键对象类型的联合:{ compilation_errors: ... } | { test_errors: ... } | { test_info: ... }。而TypeScript中,联合类型的对象只需要满足其中一个成员的结构即可,所以缺少test_info不会报错——因为传入的对象符合第一个联合成员的结构。
而最初的EntityMap<Entities>没有条件类型,直接对整个联合类型做映射,生成的是包含所有三个键的必填对象类型,所以缺少键会报错。
解决方案:禁用分布式条件类型
要避免分布式特性,只需要把泛型参数用元组包裹,让TypeScript把联合类型当成一个整体处理:
方案1:直接修改条件类型
type EntityMapAlternative<Type> = [Type] extends [boolean] ? string : { [T in Extract<Type, { type: string }> as T["type"]]: ((par: T) => any) | string | boolean | object } type ParamsAlternative = EntityMapAlternative<Entities> // 此时ParamsAlternative是包含三个必填键的对象类型,缺少test_info会报错
方案2:复用已有EntityMap类型
type EntityMapExtracted<Type> = [Type] extends [boolean] ? string : EntityMap<Type> type ParamsExtracted = EntityMapExtracted<Entities> // 同样生成包含所有必填键的对象类型
原理是:[Type]是一个元组类型,即使Type是联合类型,[Type]也会被视为单个整体类型,不会触发分布式拆分。此时条件判断会针对整个联合类型做检查,而不是每个成员,因此会进入分支EntityMap<Type>,生成预期的全必填键对象类型。
内容的提问来源于stack exchange,提问作者Jarek

