You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 20:39:18