Eldamir
Eldamir

Reputation: 10062

array.groupBy in TypeScript

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?

Upvotes: 29

Views: 96699

Answers (7)

Some random IT boy
Some random IT boy

Reputation: 8457

For new comers just use Object.groupBy.

It is now builtin into most modern runtimes.

Upvotes: 1

coreyward
coreyward

Reputation: 80041

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.

Upvotes: 0

kevinrodriguez-io
kevinrodriguez-io

Reputation: 1382

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.

Upvotes: 76

Arvind Chourasiya
Arvind Chourasiya

Reputation: 17422

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

Upvotes: 8

Delapouite
Delapouite

Reputation: 10167

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.

Upvotes: 9

Khaino
Khaino

Reputation: 4149

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(..)

Upvotes: 8

Andrew Monks
Andrew Monks

Reputation: 666

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)=>{
                    [...]
                })

Upvotes: 9

Related Questions