array.groupBy in TypeScript
Asked Answered
K

7

28

The basic array class has .map, .forEach, .filter, and .reduce, but .groupBy i noticably absent, preventing me from doing something like

const MyComponent = (props:any) => {
    return (
        <div>
            {
                props.tags
                .groupBy((t)=>t.category_name)
                .map((group)=>{
                    [...]
                })
            }

        </div>
    )
}

I ended up implementing something myself:

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
}

So now I can do

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

Works pretty well, but it is too bad that I need to wrap the list rather than just being able to chain method calls.

Is there a better solution?

Keciakeck answered 9/2, 2017 at 11:56 Comment(1)
typescript doesn't add any polyfills for the ES6 standard library changes. If you want to get those too I would suggest looking into core.js :)Laue
L
9

you could add the function to the array prototype in your app (note some don't recomend this: Why is extending native objects a bad practice?):

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

Then create an interface in typescript like this:

.d.ts version:

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

OR in a normal ts file:

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

Then you can use:

 props.tags.groupBy((t)=>t.category_name)
     .map((group)=>{
                    [...]
                })
Lavery answered 9/2, 2017 at 12:5 Comment(11)
Will the compiler automatically pick up the Array<T> interface and merge it with the existing definition? Or will I have two array-types?Keciakeck
Yep, it will pick it up. We use it in our app alotLavery
Interesting. ThanksKeciakeck
@Keciakeck "declare global" does not work in my project (TS 2) please can you explain its use? Thanks.Lavery
I'm not sure about the specifics. Without it, I got a compiler error (TS 2.1.5). I found this post which pointed me to declare global and now it works.Keciakeck
found the reason. Have updated my answer. I always tend to use definition files for these things. you do not need to refer to the global in a definition fileLavery
@Keciakeck TypeScript uses declaration merging on interfaces. They're "open-ended" which means that you can spread and extend the functionality of an interface over several declarations. This example however, which I'm not an advocate of, suggests that you bind a custom function to Array.prototype. This is bad practice, and should be avoided. What happens when the ECMAScript team decide that Array.prototype.groupBy should be a thing? - Your code breaks theirs, and vice-versa. You'd be better off extending Array and adding the functionality to your own type (class SuperArray extends Array...)Skip
I do agree to a point with @series0ne. But as long as this is only intended for use in your app, your function will override any new implementation in ECMAScript and still work.Lavery
@AndrewMonks yes that's perfectly valid. I guess it's more from a framework/library perspective. I believe that PrototypeJS was the famous example of a library, killed off because they did things that were eventually superseded in ECMAScript.Skip
One thing worth investigating in this respect is the TypeScript teams discussion about extension methods, like C#Skip
@AndrewMonks we can define Array.prototype.groupBy anywhere in the code?Alternative
E
73

You can use the following code to group stuff using 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[]>);

So, if you have the following structure and array:

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

You can invoke it like:

const results = groupBy(people, i => i.name);

Which in this case, will give you an object with string keys, and Person[] values.

There are a few key concepts here:

1- You can use function to get the key, this way you can use TS infer capabilities to avoid having to type the generic every time you use the function.

2- By using the K extends keyof any type constraint, you're telling TS that the key being used needs to be something that can be a key string | number | symbol, that way you can use the getKey function to convert Date objects into strings for example.

3- Finally, you will be getting an object with keys of the type of the key, and values of the of the array type.

Elroyels answered 6/7, 2020 at 23:29 Comment(5)
Clean and type secure, this is the proper way.Rael
Incredible. Thank you for the code and the great explanation.Involuntary
Spent a while looking for a good solution that retains type information, and this is the best one!Tolmach
How can I iterate the groups?Winzler
@Winzler Object.entries(group) gives you an array of arrays of [key, values]Elroyels
L
9

you could add the function to the array prototype in your app (note some don't recomend this: Why is extending native objects a bad practice?):

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

Then create an interface in typescript like this:

.d.ts version:

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

OR in a normal ts file:

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

Then you can use:

 props.tags.groupBy((t)=>t.category_name)
     .map((group)=>{
                    [...]
                })
Lavery answered 9/2, 2017 at 12:5 Comment(11)
Will the compiler automatically pick up the Array<T> interface and merge it with the existing definition? Or will I have two array-types?Keciakeck
Yep, it will pick it up. We use it in our app alotLavery
Interesting. ThanksKeciakeck
@Keciakeck "declare global" does not work in my project (TS 2) please can you explain its use? Thanks.Lavery
I'm not sure about the specifics. Without it, I got a compiler error (TS 2.1.5). I found this post which pointed me to declare global and now it works.Keciakeck
found the reason. Have updated my answer. I always tend to use definition files for these things. you do not need to refer to the global in a definition fileLavery
@Keciakeck TypeScript uses declaration merging on interfaces. They're "open-ended" which means that you can spread and extend the functionality of an interface over several declarations. This example however, which I'm not an advocate of, suggests that you bind a custom function to Array.prototype. This is bad practice, and should be avoided. What happens when the ECMAScript team decide that Array.prototype.groupBy should be a thing? - Your code breaks theirs, and vice-versa. You'd be better off extending Array and adding the functionality to your own type (class SuperArray extends Array...)Skip
I do agree to a point with @series0ne. But as long as this is only intended for use in your app, your function will override any new implementation in ECMAScript and still work.Lavery
@AndrewMonks yes that's perfectly valid. I guess it's more from a framework/library perspective. I believe that PrototypeJS was the famous example of a library, killed off because they did things that were eventually superseded in ECMAScript.Skip
One thing worth investigating in this respect is the TypeScript teams discussion about extension methods, like C#Skip
@AndrewMonks we can define Array.prototype.groupBy anywhere in the code?Alternative
L
9

During the TC39 meeting of December 2021, the proposal introducing the new Array.prototype.groupBy and Array.prototype.groupByToMap function has reached stage 3 in the specification process.

Here's how both functions are supposed to look like according to the README linked above:

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

While not a 100% guaranty that it will really end up in a future version of JavaScript in the form described above (there's always a chance that the proposal can be adjusted or dropped, notably for compatibility reasons), it's nevertheless a strong commitment to have this groupBy feature offered in the standard lib soon.

By ripple effect, it also means that these functions will be also available in TypeScript.

Lysippus answered 15/12, 2021 at 8:13 Comment(2)
And it already landet in Firefox v100 🎉 caniuse.com/?search=groupbyKinakinabalu
It had landed as Object.groupBy(). One comment was that it couldn't get into Array.groupBy, because there were already too many prototype extensions out there and TC39 did not want to break the web.Vocalise
C
8

A good option might be lodash.

npm install --save lodash
npm install --save-dev @types/lodash

Just import it import * as _ from 'lodash' and use.

Example

_.groupBy(..)
_.map(..)
_.filter(..)
Coco answered 13/2, 2020 at 8:42 Comment(0)
W
8

Instead of groupby use reduce. Suppose product is your 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);
Weld answered 31/8, 2020 at 13:13 Comment(0)
M
0

I needed a version of this that didn't use any, and since I only need to group on string values, I revised the solution @kevinrodriguez-io provided:

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")

This enforces that the keyProperty argument is a key of T and returns a fully typed response.

Mandrill answered 18/10, 2023 at 18:17 Comment(2)
This does not seem to work if T is an interface rather than a Record. Using T extends object in that case, worked for me.Rencontre
@Rencontre Yes, this is specifically written for instances where the input is of the specified shape. Using it with another type that happens to be an object (e.g., a function or an array) is not recommended.Mandrill
N
0

For new comers just use Object.groupBy.

It is now builtin into most modern runtimes.

Nineveh answered 11/3, 2024 at 10:14 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.