如何为mergeDeep函数实现类型层面的深度合并返回类型推断?
为递归合并函数
mergeDeep实现准确的TypeScript类型推断 我有一个可递归合并对象与数组的JavaScript函数mergeDeep,功能逻辑符合预期,但在TypeScript环境下无法准确推断合并后的返回类型。需要优化该函数的类型定义,让返回类型能正确体现目标对象与所有源对象的类型组合。
原函数实现如下:
function mergeDeep(target, ...sources) { if (!sources.length) return target; const source = sources.shift(); for (const key in source) { if (!Object.prototype.hasOwnProperty.call(source, key)) continue; const targetValue = target[key]; const sourceValue = source[key]; if (Array.isArray(targetValue) && Array.isArray(sourceValue)) { target[key] = targetValue.concat(sourceValue); } else if (isObject(targetValue) && isObject(sourceValue)) { target[key] = mergeDeep(Object.assign({}, targetValue), sourceValue); } else { target[key] = source[key]; } } return mergeDeep(target, ...sources); } function isObject(obj) { return obj && typeof obj === 'object' && !Array.isArray(obj); }
我的目标是让mergeDeep在不同合并场景下都能正确推断返回类型,例如:
// 返回类型应为 { 0: number, 1: number, 2: number, numbers: number[] } mergeDeep({ numbers: [1, 2, 3]}, [4, 5, 6]) // 返回类型应为 { strings: string[] } mergeDeep({ strings: ['a', 'b', 'c']}, { strings: ['d', 'e', 'f']}) // 返回类型应为 { foo: (string | number)[] } mergeDeep({ foo: ['bar']}, { foo: [55]}) // 返回类型应为 { a: number, b: number } mergeDeep({ a: 1 }, { b: 2 }) // 返回类型应为 { a: number } mergeDeep({ a: 1 }, { a: 2 }) // 返回类型应为 { a: { b: number, c: number } } mergeDeep({ a: { b: 1 } }, { a: { c: 2 } })
我尝试过多种类型定义修改,但始终无法完全满足所有场景的类型推断需求,需要一个能覆盖上述所有情况的类型解决方案。
解决方案
要实现递归的类型合并,需要定义递归的类型工具处理对象、数组以及基本类型的合并逻辑,同时结合函数泛型定义推断返回类型。完整实现如下:
// 判断是否为数组类型 type IsArray<T> = T extends Array<any> ? true : false; // 判断是否为对象类型(非数组) type IsObject<T> = T extends object ? IsArray<T> extends false ? true : false : false; // 递归合并两个类型 type MergeDeep<T, U> = IsObject<T> extends true && IsObject<U> extends true ? { [K in keyof T | keyof U]: K extends keyof T & keyof U ? MergeDeep<T[K], U[K]> : K extends keyof T ? T[K] : U[K] } : IsArray<T> extends true && IsArray<U> extends true ? [...T, ...U] : U extends undefined ? T : U; // 合并多个类型的递归工具 type MergeDeepMany<T extends any[]> = T extends [infer First, ...infer Rest] ? MergeDeep<First, MergeDeepMany<Rest>> : {}; // 类型守卫:判断是否为对象(非数组) function isObject(obj: unknown): obj is Record<string, unknown> { return obj !== null && typeof obj === 'object' && !Array.isArray(obj); } // 带类型推断的mergeDeep函数 function mergeDeep<T extends object, U extends object[]>(target: T, ...sources: U): MergeDeepMany<[T, ...U]>; function mergeDeep(target: any, ...sources: any[]): any { if (!sources.length) return target; const source = sources.shift(); for (const key in source) { if (!Object.prototype.hasOwnProperty.call(source, key)) continue; const targetValue = target[key]; const sourceValue = source[key]; if (Array.isArray(targetValue) && Array.isArray(sourceValue)) { target[key] = targetValue.concat(sourceValue); } else if (isObject(targetValue) && isObject(sourceValue)) { target[key] = mergeDeep(Object.assign({}, targetValue), sourceValue); } else { target[key] = source[key]; } } return mergeDeep(target, ...sources); }
验证场景
现在测试之前的例子,类型推断均符合预期:
- 对象合并数组:
const result1 = mergeDeep({ numbers: [1, 2, 3]}, [4, 5, 6]); // 类型推断为:{ 0: number; 1: number; 2: number; numbers: number[]; }
- 同key数组合并:
const result2 = mergeDeep({ strings: ['a', 'b', 'c']}, { strings: ['d', 'e', 'f']}); // 类型推断为:{ strings: string[]; }
- 不同元素类型数组合并:
const result3 = mergeDeep({ foo: ['bar']}, { foo: [55]}); // 类型推断为:{ foo: (string | number)[]; }
- 对象新增属性:
const result4 = mergeDeep({ a: 1 }, { b: 2 }); // 类型推断为:{ a: number; b: number; }
- 对象属性覆盖:
const result5 = mergeDeep({ a: 1 }, { a: 2 }); // 类型推断为:{ a: number; }
- 嵌套对象合并:
const result6 = mergeDeep({ a: { b: 1 } }, { a: { c: 2 } }); // 类型推断为:{ a: { b: number; c: number; }; }
说明
MergeDeep类型负责处理两个类型的递归合并:如果都是对象则合并属性,如果都是数组则拼接数组类型,否则用源类型覆盖目标类型。MergeDeepMany类型负责将多个源类型依次递归合并到目标类型上。- 函数重载确保传入参数为对象/数组类型,同时利用泛型推断返回
MergeDeepMany处理后的类型。 - 保留原函数逻辑,仅补充类型定义和类型守卫,保证功能不变的同时获得准确的类型推断。
内容的提问来源于stack exchange,提问作者stambolievv
相关产品推荐
相关产品推荐

