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

嵌套对象(默认配置与自定义配置)深度合并的正确类型定义

为深度合并函数定义正确的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类型的工作逻辑

  1. 对象判断:先检查T和U是否为非数组、非null的对象,是则进入递归合并逻辑
  2. 属性遍历:覆盖式处理所有来自T和U的键:
    • 键同时存在于两者时,递归合并对应属性的类型
    • 键只存在于U时,直接使用U的属性类型(符合用户修改覆盖默认配置的逻辑)
    • 键只存在于T时,保留T的属性类型
  3. 非对象直接覆盖:如果其中一方不是对象(如字符串、数字、null),直接使用U的类型完成覆盖

额外优化:避免修改原对象

原代码中的merge函数直接修改了输入的obj1,可能导致意外的副作用。解决方案中先拷贝obj1到merged变量再操作,保证原对象不受影响,更符合函数式编程的最佳实践。

内容的提问来源于stack exchange,提问作者henroper

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 09:22:37