TypeScript中的array.groupBy

28
基本的数组类有 .map, .forEach, .filter, 和 .reduce 方法,但是明显缺少了 .groupBy 方法,这使我无法像下面这样做:
const MyComponent = (props:any) => {
    return (
        <div>
            {
                props.tags
                .groupBy((t)=>t.category_name)
                .map((group)=>{
                    [...]
                })
            }

        </div>
    )
}

最终,我自己实现了一些东西:

class Group<T> {
    key:string;
    members:T[] = [];
    constructor(key:string) {
        this.key = key;
    }
}


function groupBy<T>(list:T[], func:(x:T)=>string): Group<T>[] {
    let res:Group<T>[] = [];
    let group:Group<T> = null;
    list.forEach((o)=>{
        let groupName = func(o);
        if (group === null) {
            group = new Group<T>(groupName);
        }
        if (groupName != group.key) {
            res.push(group);
            group = new Group<T>(groupName);
        }
        group.members.push(o)
    });
    if (group != null) {
        res.push(group);
    }
    return res
}

所以现在我可以做到:
const MyComponent = (props:any) => {
    return (
        <div>
            {
                groupBy(props.tags, (t)=>t.category_name)
                .map((group)=>{
                    return (
                        <ul key={group.key}>
                            <li>{group.key}</li>
                            <ul>
                                {
                                    group.members.map((tag)=>{
                                        return <li key={tag.id}>{tag.name}</li>
                                    })
                                }
                            </ul>
                        </ul>
                    )
                })
            }

        </div>
    )
}

它的表现相当好,但很遗憾我需要包装列表而不是只能链式地调用方法。

有更好的解决方案吗?


TypeScript并没有为ES6标准库的更改添加任何polyfill。如果你想要获得这些东西,我建议看一下core.js :) - toskv
6个回答

69
您可以使用以下代码来使用TypeScript对内容进行分组。
const groupBy = <T, K extends keyof any>(list: T[], getKey: (item: T) => K) =>
  list.reduce((previous, currentItem) => {
    const group = getKey(currentItem);
    if (!previous[group]) previous[group] = [];
    previous[group].push(currentItem);
    return previous;
  }, {} as Record<K, T[]>);


// A little bit simplified version
const groupBy = <T, K extends keyof any>(arr: T[], key: (i: T) => K) =>
  arr.reduce((groups, item) => {
    (groups[key(item)] ||= []).push(item);
    return groups;
  }, {} as Record<K, T[]>);


所以,如果你有以下结构和数组:
```html
Item 1
Item 2
Item 3
// 数组 const items = ['Item 1', 'Item 2', 'Item 3']; ```
type Person = {
  name: string;
  age: number;
};

const people: Person[] = [
  {
    name: "Kevin R",
    age: 25,
  },
  {
    name: "Susan S",
    age: 18,
  },
  {
    name: "Julia J",
    age: 18,
  },
  {
    name: "Sarah C",
    age: 25,
  },
];

您可以这样调用它:
const results = groupBy(people, i => i.name);

在这种情况下,您将得到一个具有字符串键和Person[]值的对象。
这里有几个关键概念:
1- 您可以使用函数来获取键,这样您就可以利用TS的推断能力,避免每次使用函数时都要输入泛型。
2- 通过使用类型约束,您告诉TS正在使用的键需要是可以成为键的东西,这样您就可以使用getKey函数将日期对象转换为字符串。
3- 最后,您将获得一个具有键类型的键和数组类型的值的对象。

3
清洁并且保证安全,这是正确的方式。 - Mathias Ducancel
1
太棒了。感谢您提供的代码和出色的解释。 - Matt.
1
花了一些时间寻找保留类型信息的好解决方案,这是最好的一个! - cor
我该如何迭代这些组? - fredyjimenezrendon
@fredyjimenezrendon Object.entries(group) 会给你一个由 [键, 值] 数组组成的数组。 - kevinrodriguez-io

9
一个不错的选择可能是lodash
npm install --save lodash
npm install --save-dev @types/lodash

只需导入它import * as _ from 'lodash'并使用。

示例

_.groupBy(..)
_.map(..)
_.filter(..)

8

在您的应用程序中,您可以将该函数添加到数组原型中(请注意,有些人不建议这样做:为什么扩展本地对象是一种不好的做法?):

Array.prototype.groupBy = function(/* params here */) { 
   let array = this; 
   let result;
   /* do more stuff here*/
   return result;
}; 

那么,请先创建一个 TypeScript 接口,内容如下:
.d.ts 版本:
    interface Array<T>
    {
        groupBy<T>(func:(x:T) => string): Group<T>[]
    }

在普通的ts文件中:

declare global {
   interface Array<T>
   {
      groupBy<T>(func:(x:T) => string): Group<T>[]
   }
}

然后您可以使用:

 props.tags.groupBy((t)=>t.category_name)
     .map((group)=>{
                    [...]
                })

编译器会自动获取Array<T>接口并将其与现有定义合并吗?还是我会有两种数组类型? - Eldamir
1
找到了原因。我已经更新了我的答案。我总是倾向于使用定义文件来处理这些事情。在定义文件中,您不需要引用全局变量。 - Andrew Monks
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Matthew Layton
在这方面值得探究的一件事是TypeScript团队关于扩展方法的讨论,比如C#。 - Matthew Layton
1
@AndrewMonks 我们可以在代码的任何地方定义 Array.prototype.groupBy 吗? - Juliatzin
显示剩余6条评论

8

不要使用groupby,而要使用reduce。假设product是你的array

let group = product.reduce((r, a) => {
console.log("a", a);
console.log('r', r);
r[a.organization] = [...r[a.organization] || [], a];
return r;
}, {});

console.log("group", group);

7

在2021年12月的TC39会议上,引入新的Array.prototype.groupByArray.prototype.groupByToMap函数的提议已经达到了规范流程的第3阶段。

以下是根据上述README所示的两个函数的外观:

const array = [1, 2, 3, 4, 5];

// groupBy groups items by arbitrary key.
// In this case, we're grouping by even/odd keys
array.groupBy((num, index, array) => {
  return num % 2 === 0 ? 'even': 'odd';
});

// =>  { odd: [1, 3, 5], even: [2, 4] }

// groupByToMap returns items in a Map, and is useful for grouping using
// an object key.
const odd  = { odd: true };
const even = { even: true };
array.groupByToMap((num, index, array) => {
  return num % 2 === 0 ? even: odd;
});

// =>  Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }

虽然不能百分之百地保证这个描述中的形式最终会成为JavaScript未来版本的一部分(提案可能因兼容性原因而进行调整或取消),但它仍然是一个强烈的承诺,即将在标准库中很快提供这个groupBy功能。

由于涟漪效应,这也意味着这些函数将在TypeScript中可用。


它已经在Firefox v100中实现了 https://caniuse.com/?search=groupby - Daniel

1
我需要一个不使用any的版本,因为我只需要对字符串值进行分组,所以我修改了@kevinrodriguez-io提供的解决方案。
const groupBy = <T extends Record<string, unknown>, K extends keyof T>(
  arr: readonly T[],
  keyProperty: K
) =>
  arr.reduce(
    (output, item) => {
      const key = String(item[keyProperty])
      output[key] ||= []
      output[key].push(item)
      return output
    },
    {} as Record<string, T[]>
  )

// Usage:
groupBy([{ foo: "bar" }], "foo")

这样做可以确保keyProperty参数是T的一个键,并返回一个完全类型化的响应。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接