typescript - 如何在 typescript 中构建类型安全的多属性groupBy

标签 typescript group-by type-safety

我已经有一个 groupBy 函数,它接收一个对象数组和一个键。它能够按单个属性进行分组。

const groupBy = <T extends Record<string, unknown>, U extends keyof T>(
  objArr: T[],
  key: U,
): { [key: string]: T[] }

由于我得到了一个 depth=1 对象,因此我对返回类型非常满意 { [key: string]: T[] }

但现在我需要扩展此函数,以便能够按多个属性进行分组并使用 depth=keys.length 创建嵌套对象。

groupBy 函数:

const groupBy = <T extends Record<string, unknown>, U extends keyof T>(
  objArr: T[],
  key: U,
): { [key: string]: T[] } => objArr
    .reduce((memo, x) => {
      if (x[key]) {
        const value = (x[key] as any).toString();
        if (!memo[value]) {
          memo[value] = [];
        }
        memo[value].push(x);
      }
      return memo;
    }, {} as { [key: string]: Array<T> });

groupByMulti 函数,仅递归调用 groupBy 函数,直到到达最后一个分组键。

const groupByMulti = <T extends Record<string, unknown>, U extends keyof T>(
  arr: T[],
  keys: U[],
  propIndex = 0,
) => {
  const grouppedObj = groupBy(arr, keys[propIndex]);
  Object.keys(grouppedObj).forEach((key) => {
    if (propIndex < keys.length - 1) {
      grouppedObj[key] = groupByMulti(grouppedObj[key], keys, propIndex + 1);
    }
  });
  return grouppedObj;
}

我想知道是否可以构建具有固定深度的对象类型,例如:

  groupByMulti(someArray, ['key1', 'key2']): {
    [key: string]: {
      [key: string]: T[]
    }
  }

// Depending on the keys length, for example

    groupByMulti(someArray, ['key1', 'key2', 'key3']): {
      [key: string]: {
        [key: string]: {
          [key: string]: T[]
        }
      }
    }

我知道只有当键是 ReadonlyArray 时这才有可能,但我可以接受。 如果不可能,我怎样才能在这里实现类型安全?

转换为JS的示例,举例说明:

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',
  },
]


const groupBy = (
    objArr,
    property,
  ) => objArr
  .reduce((memo, x) => {
    if (x[property]) {
      const value = (x[property]).toString();
      if (!memo[value]) {
        memo[value] = [];
      }
      memo[value].push(x);
    }
    return memo;
  }, {});

const groupByMulti = (arr, keys, propIndex = 0) => {
  var grouppedObj = groupBy(arr, keys[propIndex]);
  Object.keys(grouppedObj).forEach((key) => {
    if (propIndex < keys.length - 1) {
      grouppedObj[key] = groupByMulti(grouppedObj[key], keys, propIndex + 1);
    }
  });
  return grouppedObj;
}


console.log(JSON.stringify(groupByMulti(cars, ['car', 'year', 'model']), null, 2));

最佳答案

我假设您更关心调用者方面的类型安全,特别是因为您现有的 groupBy()函数至少有一个as any type assertion在其实现中。

假设 groupByMulti() 的调用签名应该看起来像:

<T extends Record<K[number], {}>, K extends readonly (keyof T)[]>(
  arr: readonly T[], 
  keys: readonly [...K],
  propIndex?: number
) => GroupByMulti<T, K>

了解 GroupByMulti 的一些适当定义.

在我们开始GroupByMulti之前,让我解释一下该调用签名的其余部分:

我们想要K成为tuple type key ,我们想要 T是属性为 K 的对象类型这些属性的值可分配给 {} ,即所谓empty object type它(尽管有名称)接受所有原语(例如 stringnumber )并且仅拒绝 nullundefined 。我这样做是因为你打电话 .toString()在这些属性上,我们可能不希望- undefinednull属性在那里。

如果您想知道 readonly XXX[] ,这是因为 readonly arrays readonly tuples比“普通”数组的限制更少。 (名字是骗人的;readonly这里的意思是“绝对可以读,但可能会也可能不会写”,而常规数组是“一定可以读,并且一定可以写”)。

最后一个事实是keys[...K]而不仅仅是K正在使用 variadic tuple types给编译器一个提示,我们希望它跟踪该数组的确切长度,因为它有很大的不同。


好的,现在让我们定义 GroupByMulti :

type GroupByMulti<T, K extends readonly any[]> =
    K extends readonly [any, ...infer KR] ?
    Record<string, GroupByMulti<T, KR>> : readonly T[];

这是一个 recursive conditional type走过K元组。如果K是空的,我们只想要readonly T[] ,也就是说,不按键分组只会给你一个数组。否则,我们想要 Record<string, GroupByMulti<T, KR>>哪里KR是一个比 K 短 1 的数组。 (这是 R 数组的 K est,所以我称之为 KR 。🤷‍♂️)。

该实现充满了类型断言,我不会花时间解释:

const groupByMulti = <
    T extends Record<K[number], {}>,
    K extends readonly (keyof T)[]
>(arr: readonly T[], keys: readonly [...K], propIndex = 0) => {
    var grouppedObj: any = groupBy(arr, keys[propIndex]);
    Object.keys(grouppedObj).forEach((key) => {
        if (propIndex < keys.length - 1) {
            grouppedObj[key] = groupByMulti<any, any[]>(
                grouppedObj[key], keys, propIndex + 1);
        } else {
            grouppedObj[key] = grouppedObj[key]
        }
    });
    return grouppedObj as GroupByMulti<T, K>;
}

唯一重要的一点是我们返回 GroupByMulti<T, K> 类型的值。让我们测试一下:


const result = groupByMulti(cars, ['car', 'year', 'model']);
/* const result: Record<string, Record<string, Record<string, readonly {
    car: string;
    model: string;
    style: string;
    year: string;
}[]>>> */

Object.keys(result).forEach(k =>
    Object.keys(result[k]).forEach(l =>
        Object.keys(result[k][l]).forEach(m => {
            result[k][l][m].forEach(c => console.log(c.car));
        })
    )
)

看起来不错。 result的类型是一个三重嵌套对象 string键,其深层对象类型是 T 的 an对象。这使我们能够以某种类型安全的方式处理结果(编译器知道 result[k][l][m] 是一个数组)。

Playground link to code

关于typescript - 如何在 typescript 中构建类型安全的多属性groupBy,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72968324/

相关文章:

typescript - 在 TypeScript 中通过装饰器向类添加属性

reflection - 尚不支持左侧为空的 Kotlin 类文字?

java - 为什么改造广告为 "Type Safe"库?

sql - 计算重复项时自加入 vs 分组

javascript - 为什么 JSON 是类型安全的?

javascript - RegExp匹配url中的查询参数

javascript - 如何从 Angular/Typescript 中的外部 javascript 文件更新变量

node.js - 关系 Y 的列 X 包含空值

sql - 在这种情况下,我需要如何更改我的 sql 才能获得我想要的内容?

sql - 返回最近的行 SQL Server