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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 12:45:43