能否为递归泛型对象映射函数添加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
相关产品推荐
相关产品推荐

