如何为更新嵌套对象属性的递归函数编写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.
问题根源
modify函数类型错误:原类型中F extends (obj: O) => O要求modify接收整个顶层对象,但实际上modify应该作用于嵌套路径的终点值,而非整个对象。- 类型推导不精确:递归时TypeScript无法将
val1的类型从宽泛的Record<string, unknown>精确推断为当前路径对应的子对象类型。 - 缺少路径值提取类型:没有定义辅助类型来根据嵌套路径提取对应的终点值类型。
修复方案
步骤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
相关产品推荐
相关产品推荐

