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 }>>;
修复关键点
- 区分any和对象:
IsObjectAndNotAny通过unknown extends T来判断T是否为any(因为unknown是所有类型的父类型,但只有any能兼容unknown),避免把any当成对象递归合并。 - 简化逻辑:移除了原代码中处理U属性时的冗余判断,让逻辑更清晰。
- 严格递归条件:只有当
T[K]和U[K]都是非数组、非any的对象时,才进行递归合并,确保优先保留T的类型。
现在所有测试用例都能正常通过,包括t11。
备注:内容来源于stack exchange,提问作者peter roe

