TypeScript泛型分组函数类型报错:'T[keyof T]'无法索引T[]
解决TypeScript递归对象数组分组函数的类型报错
我实现了一个基于泛型的递归对象数组分组函数,功能运行正常,但TypeScript抛出错误:Type 'T[keyof T]' cannot be used to index type 'T[]'。以下是我的代码、测试数据和调用方式,求解决类型报错的方案:
原函数代码
function groupFn<T extends {}>(arr: T[], fields: Array<keyof T>) { if (fields.length === 0) return arr; if (!Array.isArray(arr)) return arr; const result = arr.reduce((acc: T[], item: T) => { const trigger = item[fields[0]]; if (!acc[trigger]) { acc[trigger] = []; } acc[trigger].push(item); return acc; }, []); const fieldCopy = [...fields]; fieldCopy.splice(0, 1); fieldCopy.map(() => { Object.keys(result).map((key: string) => { const innerResults = groupFn(result[key], fieldCopy); Object.keys(innerResults).map((inner, i) => { if (i === 0) { const originalData = result[key]; result[key] = {}; result[key]["data"] = originalData; } result[key][inner] = innerResults[inner]; }); }); }); return result; }
测试数据
const people = [ { name: "Alice", age: 28, town: "nashville", job: "manager" }, { name: "Tom", age: 28, town: "hermitage", job: "manager" }, { name: "Bob", age: 30, town: "nashville", job: "dev" }, { name: "Eve", age: 28, town: "nashville", job: "dev" }, { name: "Bill", age: 28, town: "nashville", job: "manager" }, { name: "Tina", age: 30, town: "hermitage", job: "dev" }, { name: "Evan", age: 30, town: "hermitage", job: "manager" }, { name: "Cindy", age: 28, town: "hermitage", job: "manager" }, ];
调用方式
const result = groupFn(people, ["age", "town", "job"]);
错误原因及解决方案
错误根源
你把reduce的累加器acc定义为T[]类型,但实际是在把它当作键值对对象使用(用分组字段的值trigger作为键)。数组的索引只能是数字或可转换为数字的字符串,而trigger的类型是T[keyof T],可能是字符串、数字等非数组合法索引类型,因此TypeScript报错。
修改后的代码
// 定义递归分组的返回类型:要么是原数组,要么是嵌套的分组对象 type GroupResult<T> = T[] | Record<string | number | symbol, GroupResult<T> & { data?: T[] }>; function groupFn<T extends {}>(arr: T[], fields: Array<keyof T>): GroupResult<T> { if (fields.length === 0) return arr; if (!Array.isArray(arr)) return arr; // 把累加器类型改为键值对对象,而非数组 const result = arr.reduce((acc: Record<string | number | symbol, T[]>, item: T) => { const trigger = item[fields[0]]; // 确保trigger可以作为对象键(转换为字符串或数字) const key = typeof trigger === 'symbol' ? trigger : String(trigger); if (!acc[key]) { acc[key] = []; } acc[key].push(item); return acc; }, {} as Record<string | number | symbol, T[]>); const fieldCopy = fields.slice(1); // 替代splice,避免修改原数组 // 遍历分组后的每个键,递归处理剩余字段 Object.keys(result).forEach(key => { const innerResults = groupFn(result[key], fieldCopy); // 如果递归结果不是原数组,说明还有后续分组,需要重组结构 if (!Array.isArray(innerResults)) { result[key] = { data: result[key], ...innerResults } as unknown as T[]; // 类型断言适配后续赋值逻辑 } }); return result; }
关键修改点
- 定义返回类型
GroupResult<T>:明确递归返回的可能是原数组,或是带有data属性的嵌套分组对象,解决类型歧义。 - 修正累加器类型:将
acc从T[]改为Record<string | number | symbol, T[]>,匹配实际的键值对使用场景。 - 处理分组键的类型:把
trigger转换为合法的对象键类型(字符串/数字/符号),避免类型不兼容。 - 简化递归逻辑:用
slice(1)替代splice复制剩余字段,避免修改原数组;用forEach替代map(因为不需要返回值),更符合语义。 - 类型断言适配结构重组:在重组
result[key]时,用类型断言让TypeScript认可结构的变更。
内容的提问来源于stack exchange,提问作者C5m7b4
相关产品推荐
相关产品推荐

