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

能否为递归泛型对象映射函数添加TypeScript类型定义?

为递归泛型对象映射函数添加TypeScript类型定义

当然可以给这个递归对象映射函数添加完整的TypeScript类型定义!我们可以通过递归泛型类型来准确描述输入输出的类型关系,同时修正原函数里的一个逻辑错误让它更可靠。

第一步:定义递归类型转换工具

首先我们需要一个泛型类型,用来递归地将输入类型中符合条件的类型替换成目标类型:

type ReplaceInObject<T, S, R> = 
  // 如果当前类型是要替换的类型S,直接替换为R
  T extends S ? R :
  // 如果是数组,递归处理数组中的每个元素
  T extends Array<infer U> ? Array<ReplaceInObject<U, S, R>> :
  // 如果是对象(排除null和数组),递归处理每个属性
  T extends object ? { [K in keyof T]: ReplaceInObject<T[K], S, R> } :
  // 其他基础类型保持不变
  T;

这个类型会逐层遍历你的对象、数组,把所有匹配S的类型替换成R,完美匹配函数的递归逻辑。

第二步:给函数添加类型定义并修正逻辑

原函数里有个小bug:遍历对象属性时判断Array.isArray(object)是错误的(这里的object是上层的对象,不是当前的val)。我们先修正这个问题,再给函数加上泛型类型:

function replaceInObject<S, R>(
  shouldReplace: (val: unknown) => val is S,
  fn: (val: S) => R
) {
  const recurse = <T>(object: T): ReplaceInObject<T, S, R> => {
    // 先处理数组情况:递归处理每个元素
    if (Array.isArray(object)) {
      return object.map(recurse) as ReplaceInObject<T, S, R>;
    }
    // 处理非对象(包括null)的情况:判断是否需要替换
    if (typeof object !== 'object' || object === null) {
      return shouldReplace(object) ? fn(object) as ReplaceInObject<T, S, R> : object as ReplaceInObject<T, S, R>;
    }
    // 处理普通对象:遍历每个属性递归处理
    for (const [key, val] of Object.entries(object)) {
      if (shouldReplace(val)) {
        (object as Record<string, unknown>)[key] = fn(val);
      } else {
        (object as Record<string, unknown>)[key] = recurse(val);
      }
    }
    return object as ReplaceInObject<T, S, R>;
  };
  return recurse;
}

关键细节说明:

  • 函数replaceInObject接受两个泛型参数:S是要被替换的类型,R是替换后的类型
  • 返回的recurse函数是泛型的,能自动推断输入对象的类型T,并返回转换后的ReplaceInObject<T, S, R>类型
  • 用as做类型断言是因为TypeScript无法完全自动推断递归类型的转换结果,这在递归泛型场景中是合理的

第三步:验证你的示例场景

现在用你提供的Map转对象的示例来测试,TypeScript会自动推断出正确的输出类型:

const isEs6Map = (val: unknown): val is Map<any, any> => val instanceof Map;
const es6MapToObject = <K, V>(map: Map<K, V>): { [key: string]: V } => {
  const obj: { [key: string]: V } = {};
  for (const [key, val] of map) obj[key.toString()] = val;
  return obj;
};

const marshal = replaceInObject(isEs6Map, es6MapToObject);

const sampleInput = {
  m: new Map([['a', 1], ['b', 2]]),
  meta: 'info',
  children: [
    { m: new Map([['c', 3], ['d', 4]]), meta: 'child' }
  ]
};

const serializedData = marshal(sampleInput);
// 此时serializedData的类型会被自动推断为:
// {
//   m: { [key: string]: number };
//   meta: string;
//   children: Array<{
//     m: { [key: string]: number };
//     meta: string;
//   }>;
// }

额外优化建议

  • 避免原地修改:原函数会直接修改输入对象,如果需要保持原对象不变,可以在处理数组和对象时创建副本:
    // 数组处理改为创建新数组
    if (Array.isArray(object)) {
      return [...object.map(recurse)] as ReplaceInObject<T, S, R>;
    }
    // 对象处理改为创建新对象
    const newObj = { ...object };
    for (const [key, val] of Object.entries(newObj)) {
      // ...处理逻辑
    }
    return newObj as ReplaceInObject<T, S, R>;
    
  • 严格类型守卫:确保shouldReplace的类型守卫足够准确,这样TypeScript才能正确识别需要替换的值,避免类型推断错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 08:53:12