TypeScript联合类型与映射类型在有无泛型时的推导差异问题
TypeScript 联合类型映射差异问题解答
核心复现代码如下:
type BaseA = { type: 'A' name: string flag: number } type BaseB = { type: 'B' id: number flag: number } type Base = BaseA | BaseB // 直接映射得到单对象类型 type w0 = { [k in keyof Base]: Base[k] } // w0 推导结果:{ type: "A" | "B"; flag: number; } // 泛型包装后映射得到联合类型 type Calc<W> = { [k in keyof W]: W[k] } type w1 = Calc<Base> // w1 推导结果:Calc<BaseA> | Calc<BaseB> type z0 = Exclude<w0,BaseA> // 推导结果:z0 = w0 type z1 = Exclude<w1,BaseA> // 推导结果:z1 = BaseB
问题1:w0与w1推导结果差异的底层原理
核心原因是TS内置的泛型同态映射类型分布式处理规则,和条件类型的分布式特性逻辑一致,目的是让泛型处理联合类型时尽可能保留每个成员的完整结构:
- 当你直接对一个已经确定的具体联合类型做
[k in keyof 联合类型]映射时,TS会先计算keyof 联合类型的结果——这个结果只会包含联合所有成员的公共属性键,比如这里Base是BaseA|BaseB,公共键只有type和flag,name和id是单个成员独有的属性,不会出现在keyof结果里。之后TS会遍历这些公共键,值类型取联合类型对应键的所有可能值的联合,最终输出单个合并后的对象类型,也就是w0的结果。 - 当映射类型被包装在泛型中,且映射逻辑是
[k in keyof 裸类型参数]的同态形式时,TS会自动触发分布逻辑:把传入的联合类型拆成单个成员,逐个代入泛型计算映射结果,最后把所有计算结果重新联合成最终类型。所以Calc<Base>会被自动拆成Calc<BaseA> | Calc<BaseB>,两个结果各自保留了自己的独有属性name和id。
问题2:不使用泛型得到等价于Calc<BaseA> | Calc<BaseB>的结果
非泛型场景下TS不会自动触发映射的分布式处理,你只需要手动把联合类型拆成单个成员,分别做映射后再联合即可,全程不需要定义任何泛型工具类型:
type w1_no_generic = | { [k in keyof BaseA]: BaseA[k] } | { [k in keyof BaseB]: BaseB[k] }
因为示例里的映射是恒等映射,上述写法结果和BaseA | BaseB(也就是Base类型)完全等价;如果你的映射逻辑有额外修改(比如加只读修饰、修改值类型),只需要同步修改两个分支的映射逻辑即可,效果和泛型分布计算完全一致。
问题3:借助泛型实现等价于w0的合并单对象类型
要在泛型中得到w0那样的公共属性合并结果,核心是阻断泛型映射类型的默认分布式行为,两种最常用的写法:
- 写法1:用as键重映射语法,直接阻断同态映射的分布触发条件
type MergeUnion<U> = { [k in keyof U as k]: U[k] }
- 写法2:通过Pick提前固定要遍历的键范围,绕开同态分布的触发逻辑
type MergeUnion2<U> = Pick<U, keyof U>
两种写法传入Base得到的结果都和w0完全一致,不会拆分成联合类型,后续做Exclude等类型运算时表现也和w0完全相同。
内容的提问来源于stack exchange,提问作者YeXiaoRain
相关产品推荐
相关产品推荐

