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

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;
}

关键修改点

  1. 定义返回类型GroupResult<T>:明确递归返回的可能是原数组,或是带有data属性的嵌套分组对象,解决类型歧义。
  2. 修正累加器类型:将acc从T[]改为Record<string | number | symbol, T[]>,匹配实际的键值对使用场景。
  3. 处理分组键的类型:把trigger转换为合法的对象键类型(字符串/数字/符号),避免类型不兼容。
  4. 简化递归逻辑:用slice(1)替代splice复制剩余字段,避免修改原数组;用forEach替代map(因为不需要返回值),更符合语义。
  5. 类型断言适配结构重组:在重组result[key]时,用类型断言让TypeScript认可结构的变更。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 20:51:18