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

如何为更新嵌套对象属性的递归函数编写TypeScript类型?

嵌套对象递归更新函数的TypeScript类型问题修复

问题背景

我正在Node.js上开发一个私有项目,原本使用JavaScript编写,现在迁移到TypeScript。项目中有一些基于《Grokking Functional Programming》一书示例的工具函数,正在为它们添加类型定义。在给可更新嵌套对象属性的递归函数编写类型签名时遇到了瓶颈,需要解决类型推导错误的问题。

现有代码

update函数(单层属性更新)

type Update = <
  O extends Record<string, unknown>,
  K extends keyof O,
  F extends (val: O[K]) => O[K]
>(
  obj: O,
  key: K,
  modify: F
) => O;

const update: Update = (obj, key, modify) => {
  const copy = { ...obj };
  modify(copy[key]); // 注意:这里如果是基本类型,modify不会生效,正确写法应该是 copy[key] = modify(copy[key])
  return copy;
};

nestedUpdate函数(嵌套属性递归更新)

const nestedUpdate: NestedUpdate = (obj, keys, modify) => {
  if (keys.length === 0) return modify(obj);
  const [key1, ...restOfKeys] = keys;
  return update(obj, key1, (val1) => nestedUpdate(val1, restOfKeys, modify));
};

尝试的类型签名

type DeepKeys<T extends Record<string, unknown>> = {
  [P in keyof T]: T[P] extends Record<string, unknown>
    ? [P] | [P, ...DeepKeys<T[P]>]
    : [P];
}[keyof T];

type NestedUpdate = <
  O extends Record<string, unknown>,
  K extends DeepKeys<O>,
  F extends (obj: O) => O
>(
  obj: O,
  keys: K,
  modify: F
) => O;

错误信息

在递归调用nestedUpdate时出现以下TypeScript错误:

Type 'Record<string, unknown>' is not assignable to type 'O[keyof O]'.

  'Record<string, unknown>' is assignable to the constraint of type 'O[keyof O]', but 'O[keyof O]' could be instantiated with a different subtype of constraint 'unknown'.ts(2322)

update.ts(6, 13): The expected type comes from the return type of this signature.

问题根源

  1. modify函数类型错误:原类型中F extends (obj: O) => O要求modify接收整个顶层对象,但实际上modify应该作用于嵌套路径的终点值,而非整个对象。
  2. 类型推导不精确:递归时TypeScript无法将val1的类型从宽泛的Record<string, unknown>精确推断为当前路径对应的子对象类型。
  3. 缺少路径值提取类型:没有定义辅助类型来根据嵌套路径提取对应的终点值类型。

修复方案

步骤1:定义路径值提取辅助类型

创建PathValue类型,用于根据嵌套路径从对象中提取终点值的类型:

type PathValue<T, P extends Array<keyof any>> = 
  P extends [infer First, ...infer Rest]
    ? First extends keyof T
      ? Rest extends Array<keyof any>
        ? PathValue<T[First], Rest>
        : T[First]
      : never
    : T;

步骤2:修正NestedUpdate类型

调整泛型参数,让modify函数接收路径终点的类型,而非整个顶层对象:

type NestedUpdate = <
  O extends Record<string, unknown>,
  K extends DeepKeys<O>,
  F extends (val: PathValue<O, K>) => PathValue<O, K>
>(
  obj: O,
  keys: K,
  modify: F
) => O;

步骤3:修正nestedUpdate实现并优化类型推导

将nestedUpdate改为泛型函数写法,配合类型断言帮助TypeScript精确推导递归时的子类型:

function nestedUpdate<
  O extends Record<string, unknown>,
  K extends DeepKeys<O>
>(
  obj: O,
  keys: K,
  modify: (val: PathValue<O, K>) => PathValue<O, K>
): O {
  if (keys.length === 0) {
    // 当路径为空时,直接修改顶层对象
    return modify(obj as PathValue<O, K>) as O;
  }
  const [key1, ...restOfKeys] = keys;
  return update(obj, key1, (val1) => 
    // 断言val1为当前key对应的子类型,restOfKeys为该子类型的深层路径
    nestedUpdate(
      val1 as PathValue<O, [typeof key1]>,
      restOfKeys as DeepKeys<PathValue<O, [typeof key1]>>,
      modify
    )
  );
}

步骤4:修复update函数的逻辑问题

原update函数中直接调用modify(copy[key])对基本类型无效,应改为赋值操作:

const update: Update = (obj, key, modify) => {
  const copy = { ...obj };
  copy[key] = modify(copy[key]); // 赋值modify返回的新值
  return copy;
};

测试示例

现在可以正常使用类型安全的嵌套更新:

const testObj = {
  a: {
    b: {
      c: 123
    }
  }
};

// 类型安全:modify接收number类型,返回number类型
const updated = nestedUpdate(testObj, ['a', 'b', 'c'], (val) => val + 1);
// updated.a.b.c 类型为number,值为124

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 10:55:39