TypeScript中如何实现类型安全的多属性嵌套groupBy函数
固定嵌套深度返回类型实现方案
完全可以实现和传入分组键数量严格匹配的固定深度嵌套返回类型,核心依赖两个能力:
- 用递归条件类型构造对应层数的嵌套对象结构
- 将
keys参数约束为只读元组类型,让TypeScript可以推断传入键的精确数量与顺序
第一步:定义递归类型工具
先写一个类型工具递归生成嵌套分组结构,逻辑是:如果还有剩余分组键,就生成一层字符串索引对象,值的类型用剩余键继续递归;如果键遍历完成,就返回最内层的T[]数组类型:
type NestedGroup<T, Keys extends readonly PropertyKey[]> = Keys extends readonly [unknown, ...infer RestKeys] ? RestKeys extends readonly PropertyKey[] ? Record<string, NestedGroup<T, RestKeys>> : never : T[];
第二步:改造函数类型与实现
对原有函数做少量类型调整即可,这里用TypeScript 5.0+支持的const类型参数修饰符,调用时直接传入字面量键数组就会自动推断为只读元组,不需要手动加as const:
// 原有单键分组函数仅做类型优化,逻辑不变 const groupBy = <T extends Record<string, unknown>, U extends keyof T>( objArr: T[], key: U, ): Record<string, T[]> => objArr .reduce((memo, x) => { const keyVal = x[key]; // 修复原逻辑会过滤0、空字符串等falsy值的问题 if (keyVal !== undefined && keyVal !== null) { const value = String(keyVal); if (!memo[value]) memo[value] = []; memo[value].push(x); } return memo; }, {} as Record<string, T[]>); // 多键分组函数 const groupByMulti = < T extends Record<string, unknown>, const U extends readonly (keyof T)[] >( arr: T[], keys: U, propIndex = 0, ): NestedGroup<T, U> => { const grouppedObj = groupBy(arr, keys[propIndex]) as any; Object.keys(grouppedObj).forEach((key) => { if (propIndex < keys.length - 1) { grouppedObj[key] = groupByMulti(grouppedObj[key], keys, propIndex + 1); } }); return grouppedObj; };
如果使用低于5.0的TypeScript版本,去掉类型参数前的const修饰符,调用时给键数组加as const断言即可获得同样的类型推断效果。
使用效果
传入不同长度的键数组时,TS会自动推断对应深度的返回类型:
const cars = [ { car: 'Audi', model: 'A6', style: 'Sedan', year: '2005' }, { car: 'Audi', model: 'A4', style: 'Sedan', year: '2018' }, { car: 'Toyota', model: 'Corola', style: 'Sedan', year: '2006' }, { car: 'Toyota', model: 'Camry', style: 'Sedan', year: '2006' }, ]; // 传入2个键,返回2层嵌套:Record<string, Record<string, typeof cars[number][]>> const twoLevelResult = groupByMulti(cars, ['car', 'year']); // 传入3个键,返回3层嵌套:Record<string, Record<string, Record<string, typeof cars[number][]>>> const threeLevelResult = groupByMulti(cars, ['car', 'year', 'model']);
访问各层级属性时会有完整的类型提示,类型不匹配时TS会直接报错。
动态键场景的类型安全兜底
如果传入的keys是动态生成的普通数组、TS无法推断精确的元组长度,可以给函数增加重载,返回通用嵌套类型避免返回any:
// 重载:动态键数组场景返回通用嵌套类型 function groupByMulti<T extends Record<string, unknown>>( arr: T[], keys: (keyof T)[], propIndex?: number ): Record<string, T[] | Record<string, unknown>>; // 上述固定深度的函数实现放在重载声明之后即可
内容的提问来源于stack exchange,提问作者Omar Omeiri
相关产品推荐
相关产品推荐

