嵌套对象(默认配置与自定义配置)深度合并的正确类型定义
为深度合并函数定义正确的TypeScript类型
这确实是TypeScript里处理深度合并时常见的类型难题——直接用T & U只能处理浅层次的合并,嵌套对象的属性冲突会导致类型变成never,完全不符合我们的预期。我们需要自定义一个递归的深度合并类型来准确描述合并后的结构,同时调整函数的泛型参数来使用这个类型。
完整解决方案代码
// 定义递归的深度合并类型工具 type DeepMerge<T, U> = // 先判断T和U是否为非数组、非null的对象类型 T extends object ? U extends object ? { // 遍历T和U的所有键 [K in keyof T | keyof U]: K extends keyof U ? K extends keyof T // 键同时存在于两者中,递归合并属性类型 ? DeepMerge<T[K], U[K]> // 键只存在于U中,直接取U的属性类型 : U[K] // 键只存在于T中,保留T的属性类型 : T[K] } // U不是对象时,直接用U的类型(覆盖T) : U // T不是对象时,直接用U的类型(覆盖T) : U; const defaultConfig = { foo: 'bar', method: 'GET', headers: { Authorization: 'TOKEN', Locale: 'en-US' } }; const changes = { method: 'POST', headers: { Locale: null, Id: 2550 } }; // 调整merge函数的泛型,返回DeepMerge后的类型 const merge = <T, U>(obj1: T, obj2: U): DeepMerge<T, U> => { // 拷贝原对象避免副作用,同时断言为合并后的类型 const merged = { ...obj1 } as DeepMerge<T, U>; Object.entries(obj2).forEach(([k, v]) => { const key = k as keyof U; // 判断当前值是否为非数组、非null的对象,是则递归合并 if (v !== null && typeof v === 'object' && !Array.isArray(v)) { merged[k as keyof DeepMerge<T, U>] = merge( (merged[k as keyof T] as unknown) || {}, v ) as DeepMerge<T[Extract<keyof T, keyof U>], U[keyof U]>; } else { // 非对象类型直接覆盖 merged[k as keyof DeepMerge<T, U>] = v as unknown as DeepMerge<T, U>[keyof DeepMerge<T, U>]; } }); return merged; }; // 测试合并结果,类型完全符合预期 const newConfig = merge(defaultConfig, changes); // newConfig的类型自动推导为: // { // foo: string; // method: string; // headers: { // Authorization: string; // Locale: null; // Id: number; // }; // }
关键细节解释
为什么T & U无法满足需求?
对于嵌套的headers属性:
defaultConfig.headers的类型是{ Authorization: string; Locale: string }changes.headers的类型是{ Locale: null; Id: number }T & U会让headers变成两个类型的交集,其中Locale的类型是string & null——这在TypeScript中是never类型(没有值能同时满足字符串和null),导致整个headers类型出现错误,完全不符合我们“合并后Locale为null、保留Authorization、新增Id”的预期。
DeepMerge类型的工作逻辑
- 对象判断:先检查
T和U是否为非数组、非null的对象,是则进入递归合并逻辑 - 属性遍历:覆盖式处理所有来自
T和U的键:- 键同时存在于两者时,递归合并对应属性的类型
- 键只存在于
U时,直接使用U的属性类型(符合用户修改覆盖默认配置的逻辑) - 键只存在于
T时,保留T的属性类型
- 非对象直接覆盖:如果其中一方不是对象(如字符串、数字、null),直接使用
U的类型完成覆盖
额外优化:避免修改原对象
原代码中的merge函数直接修改了输入的obj1,可能导致意外的副作用。解决方案中先拷贝obj1到merged变量再操作,保证原对象不受影响,更符合函数式编程的最佳实践。
内容的提问来源于stack exchange,提问作者henroper
相关产品推荐
相关产品推荐

