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

如何为合并多对象的动态递归函数正确添加TypeScript类型以获得自动补全提示

解决TypeScript合并端点函数的类型提示问题

嘿,我来帮你搞定这个TypeScript类型提示的问题!你的核心困扰是mergeEndpoints能正确合并对象,但TypeScript没法推断出合并后的具体结构,导致没有自动补全提示。原代码里的泛型尝试没抓对重点,下面给你两种可行的解决方案:

方案一:自动推断合并后的类型(无需手动维护类型)

这种方式让TypeScript自动捕获所有传入端点的类型,递归计算出合并后的完整结构,不用你手动写类型定义:

// 递归合并多个对象类型的工具类型
type Merge<T extends object[]> = T extends [infer First, ...infer Rest]
  ? First extends object
    ? Merge<Rest> extends object
      ? { 
          [K in keyof First | keyof Merge<Rest>]: 
            K extends keyof First 
              ? K extends keyof Merge<Rest> 
                ? MergeDeep<First[K], Merge<Rest>[K]> 
                : First[K] 
              : K extends keyof Merge<Rest> 
                ? Merge<Rest>[K] 
                : never 
        }
      : First
    : Merge<Rest>
  : {};

// 处理嵌套对象的深层合并
type MergeDeep<A, B> = A extends object
  ? B extends object
    ? Merge<[A, B]>
    : B
  : B;

const mergeEndpoints = <T extends object[]>(endpoints: [...T]): Merge<T> => {
  const result = {} as Merge<T>;
  
  const recursiveMerge = (obj: any, entries: [string, any][]) => {
    for (const [key, value] of entries) {
      if (typeof value === 'object' && value !== null) {
        obj[key] = obj[key] ? { ...obj[key] } : {};
        recursiveMerge(obj[key], Object.entries(value));
      } else {
        obj[key] = value;
      }
    }
    return obj;
  };

  endpoints.forEach((endpoint) => {
    recursiveMerge(result, Object.entries(endpoint) as [string, any][]);
  });

  return result;
};

// 原端点函数保持不变
const endpointsA = (base: string) => ({
  GET: {
    FEATURE_A: {
      SOME_PATH: `${base}/featureA`,
    },
  },
});
const endpointsB = (base: string) => ({
  GET: {
    FEATURE_B: {
      SOME_PATH: `${base}/featureB`,
    },
  },
  POST: {
    FEATURE_B: {
      SOME_PATH: `${base}/featureB`,
    },
  },
});

const base = '/api';
const endpoints = mergeEndpoints([
  endpointsA(base),
  endpointsB(base),
]);

// 现在你能获得完整的自动补全提示了!
console.log(endpoints.GET.FEATURE_A.SOME_PATH);

原理说明

  • Merge工具类型会遍历传入的对象数组,合并所有对象的键,同时处理键冲突时的深层嵌套合并逻辑。
  • MergeDeep负责处理嵌套对象的合并,确保深层结构也能被TypeScript正确推断。
  • 函数的泛型<T extends object[]>捕获传入的端点数组类型,通过Merge<T>直接返回合并后的类型,仅在初始化result时做一次类型断言。

方案二:手动定义合并后的类型(你最终采用的方式)

如果需要更明确地控制类型结构,或者你的端点逻辑比较复杂,手动定义合并类型会更可靠:

// 先定义每个端点的具体类型
type A = {
  GET: {
    FEATURE_A: {
      SOME_PATH: string;
    };
  };
};

type B = {
  GET: {
    FEATURE_B: {
      SOME_PATH: string;
    };
  };
  POST: {
    FEATURE_B: {
      SOME_PATH: string;
    };
  };
};

// 合并所有端点类型
type TEndpoints = A & B;

// 修改mergeEndpoints函数,接收泛型T并返回该类型
const mergeEndpoints = <T>(endpoints: Array<Record<string, unknown>>): T => {
  const result = {} as T;
  
  const recursiveMerge = ({ obj, entries }: { 
    obj: Record<string, unknown>; 
    entries: Array<[string, unknown]>; 
  }) => {
    for (const [key, value] of entries) {
      if (typeof value === 'object' && value !== null) {
        obj[key] = obj[key] ? { ...obj[key] } : {};
        recursiveMerge({
          obj: obj[key] as Record<string, unknown>,
          entries: Object.entries(value) as Array<[string, unknown]>,
        });
      } else {
        obj[key] = value;
      }
    }
    return obj;
  };

  endpoints.forEach((endpoint) => {
    recursiveMerge({ obj: result as unknown as Record<string, unknown>, entries: Object.entries(endpoint) });
  });

  return result;
};

// 给端点函数指定明确的返回类型
const endpointsA = (base: string): A => ({
  GET: {
    FEATURE_A: {
      SOME_PATH: `${base}/featureA`,
    },
  },
});

const endpointsB = (base: string): B => ({
  GET: {
    FEATURE_B: {
      SOME_PATH: `${base}/featureB`,
    },
  },
  POST: {
    FEATURE_B: {
      SOME_PATH: `${base}/featureB`,
    },
  },
});

const base = '/api';
const endpoints = mergeEndpoints<TEndpoints>([
  endpointsA(base),
  endpointsB(base),
]);

// 现在同样能获得完整的自动补全!
console.log(endpoints.POST.FEATURE_B.SOME_PATH);

原理说明

  • 我们给每个端点函数指定了明确的返回类型(A、B),再通过交集类型TEndpoints = A & B得到合并后的完整类型。
  • mergeEndpoints函数通过泛型<T>接收这个合并后的类型,将结果断言为T,让TypeScript明确知道返回值的结构,从而提供自动补全提示。

总结

  • 如果你不想手动维护类型,方案一的自动推断更适合,它会随着端点函数的变化自动更新类型。
  • 如果你需要严格控制类型结构,或者有复杂的类型约束,方案二的手动定义方式更稳定。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 12:22:38