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

TypeScript递归对象合并泛型问题:排查MergeTypesWithPriority的t11测试用例失败并修复

TypeScript递归对象合并泛型问题:排查MergeTypesWithPriority的t11测试用例失败并修复

我来帮你分析这个问题,先明确你的核心需求:MergeTypesWithPriority<T, U>要优先保留T的类型,同时对嵌套对象做递归合并,但测试用例t11失败了——它期望MergeTypesWithPriority<{ a: Items }, { a: any }>返回{ a: Items },但原代码输出的是{ a: any }。

先看下你的原实现和测试用例:

type Expect<T extends true> = T
type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false

type MergeTypesWithPriority<T, U> = {
  [K in keyof T | keyof U]: K extends keyof T
      ? K extends keyof U
          ? T[K] extends object
              ? U[K] extends object
                  ? T[K] extends any[]
                      ? T[K]
                      : U[K] extends any[]
                          ? U[K]
                          : MergeTypesWithPriority<T[K], U[K]>
                  : T[K]
              : T[K]
          : T[K]
      : K extends keyof U
          ? U[K] extends object
              ? T extends Record<K, any> 
                  ? U[K] extends any
                      ? T[K]
                      : U[K]
                  : U[K]
              : U[K]
          : never;
};


type t1 = Expect<Equal<MergeTypesWithPriority<{ a: string }, { a: number }>, { a: string }>>;
type t2 = Expect<Equal<MergeTypesWithPriority<{ }, { a: number }>, { a: number }>>;
type t3 = Expect<Equal<MergeTypesWithPriority<{ a: number | undefined }, { a: number }>, { a: number | undefined }>>;
type t4 = Expect<Equal<MergeTypesWithPriority<{ a: { b: number } }, { a: { b: string } }>, { a: { b: number } }>>;
type t5 = Expect<Equal<MergeTypesWithPriority<{ a: { c: number } }, { a: { b: string } }>, { a: { b: string; c: number } }>>;
type t6 = Expect<Equal<MergeTypesWithPriority<{ a: string }, { a: any }>, { a: string }>>;
type t7 = Expect<Equal<MergeTypesWithPriority<{ a?: number }, { a: any }>, { a: number | undefined }>>;

interface Items { foo: number; bar: { baz: number[] }}
type t8 = Expect<Equal<MergeTypesWithPriority<{ a: Items[] }, { a: any[] }>, { a: Items[] }>>;
type t9 = Expect<Equal<MergeTypesWithPriority<{ a: string }, { a: Items }>, { a: string }>>;
type t10 = Expect<Equal<MergeTypesWithPriority<{ a: any }, { a: Items }>, { a: any }>>;
type t11 = Expect<Equal<MergeTypesWithPriority<{ a: Items }, { a: any }>, { a: Items }>>;

为什么t11会失败?

问题出在TypeScript对any的特殊处理:当U[K]是any时,U[K] extends object会被判定为true(因为any兼容所有类型),于是代码会进入递归合并分支MergeTypesWithPriority<T[K], U[K]>,也就是MergeTypesWithPriority<Items, any>。

在递归过程中,keyof any是string | number | symbol,所以合并后的类型会是Items加上所有可能的键值对(类型为any),最终整个类型会被降级为any,这就和t11期望的Items不符了。

另外,原代码中处理U独有的属性时还有一段冗余且错误的逻辑:T extends Record<K, any> ? U[K] extends any ? T[K] : U[K] : U[K]——既然K不在T里,T extends Record<K, any>必然是false,这段判断完全没必要,还容易引入bug。

修复方案

我们需要新增一个辅助类型来区分真正的对象类型和any,然后修改递归条件,确保只有当双方都是非any的对象时才递归合并,否则直接优先保留T的类型。

修复后的代码如下:

type Expect<T extends true> = T
type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false

// 辅助类型:判断是否是非any的对象类型
type IsObjectAndNotAny<T> = 
  T extends object 
    ? unknown extends T 
      ? false // 如果unknown能extends T,说明T是any
      : true 
    : false;

type MergeTypesWithPriority<T, U> = {
  [K in keyof T | keyof U]: K extends keyof T
      ? K extends keyof U
          // 优先处理数组:直接保留T的数组类型
          ? T[K] extends any[]
              ? T[K]
              // 只有当T[K]和U[K]都是非any的对象时,才递归合并
              : IsObjectAndNotAny<T[K]>
                ? IsObjectAndNotAny<U[K]>
                  ? MergeTypesWithPriority<T[K], U[K]>
                  : T[K] // U[K]不是合法对象,保留T的类型
                : T[K] // T[K]不是合法对象,直接保留
          : T[K] // K只在T里,直接保留T的类型
      : K extends keyof U
          ? U[K] // K只在U里,直接用U的类型
          : never;
};

// 所有测试用例验证
type t1 = Expect<Equal<MergeTypesWithPriority<{ a: string }, { a: number }>, { a: string }>>;
type t2 = Expect<Equal<MergeTypesWithPriority<{ }, { a: number }>, { a: number }>>;
type t3 = Expect<Equal<MergeTypesWithPriority<{ a: number | undefined }, { a: number }>, { a: number | undefined }>>;
type t4 = Expect<Equal<MergeTypesWithPriority<{ a: { b: number } }, { a: { b: string } }>, { a: { b: number } }>>;
type t5 = Expect<Equal<MergeTypesWithPriority<{ a: { c: number } }, { a: { b: string } }>, { a: { b: string; c: number } }>>;
type t6 = Expect<Equal<MergeTypesWithPriority<{ a: string }, { a: any }>, { a: string }>>;
type t7 = Expect<Equal<MergeTypesWithPriority<{ a?: number }, { a: any }>, { a: number | undefined }>>;

interface Items { foo: number; bar: { baz: number[] }}
type t8 = Expect<Equal<MergeTypesWithPriority<{ a: Items[] }, { a: any[] }>, { a: Items[] }>>;
type t9 = Expect<Equal<MergeTypesWithPriority<{ a: string }, { a: Items }>, { a: string }>>;
type t10 = Expect<Equal<MergeTypesWithPriority<{ a: any }, { a: Items }>, { a: any }>>;
type t11 = Expect<Equal<MergeTypesWithPriority<{ a: Items }, { a: any }>, { a: Items }>>;

修复关键点

  1. 区分any和对象:IsObjectAndNotAny通过unknown extends T来判断T是否为any(因为unknown是所有类型的父类型,但只有any能兼容unknown),避免把any当成对象递归合并。
  2. 简化逻辑:移除了原代码中处理U属性时的冗余判断,让逻辑更清晰。
  3. 严格递归条件:只有当T[K]和U[K]都是非数组、非any的对象时,才进行递归合并,确保优先保留T的类型。

现在所有测试用例都能正常通过,包括t11。

备注:内容来源于stack exchange,提问作者peter roe

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.15 09:23:09