JavaScript is getting array grouping methods

Phil Nash - Sep 18 '23 - - Dev Community

Grouping items in an array is one of those things you've probably done a load of times. Each time you would have written a grouping function by hand or perhaps reached for lodash's groupBy function.

The good news is that JavaScript is now getting grouping methods so you won't have to anymore. Object.groupBy and Map.groupBy are new methods that will make grouping easier and save us time or a dependency.

Grouping until now

Let's say you have an array of objects representing people and you want to group them by their age. You might use a forEach loop like this:

const people = [
  { name: "Alice", age: 28 },
  { name: "Bob", age: 30 },
  { name: "Eve", age: 28 },
];

const peopleByAge = {};

people.forEach((person) => {
  const age = person.age;
  if (!peopleByAge[age]) {
    peopleByAge[age] = [];
  }
  peopleByAge[age].push(person);
});
console.log(peopleByAge);
/*
{
  "28": [{"name":"Alice","age":28}, {"name":"Eve","age":28}],
  "30": [{"name":"Bob","age":30}]
}
*/
Enter fullscreen mode Exit fullscreen mode

Or you may choose to use reduce, like this:

const peopleByAge = people.reduce((acc, person) => {
  const age = person.age;
  if (!acc[age]) {
    acc[age] = [];
  }
  acc[age].push(person);
  return acc;
}, {});
Enter fullscreen mode Exit fullscreen mode

Either way, it's slightly awkward code. You always have to check the object to see whether the grouping key exists and if not, create it with an empty array. Then you can push the item into the array.

Grouping with Object.groupBy

With the new Object.groupBy method, you can outcome like this:

const peopleByAge = Object.groupBy(people, (person) => person.age);
Enter fullscreen mode Exit fullscreen mode

Much simpler! Though there are some things to be aware of.

Object.groupBy returns a null-prototype object. This means the the object does not inherit any properties from Object.prototype. This is great because it means you won't accidentally overwrite any properties on Object.prototype, but it also means that the object doesn't have any of the methods you might expect, like hasOwnProperty or toString.

const peopleByAge = Object.groupBy(people, (person) => person.age);
console.log(peopleByAge.hasOwnProperty("28"));
// TypeError: peopleByAge.hasOwnProperty is not a function
Enter fullscreen mode Exit fullscreen mode

The callback function you pass to Object.groupBy should return a string or a Symbol. If it returns anything else, it will be coerced to a string.

In our example, we have been returning the age as a number, but in the result it is coerced to string. Though you can still access the properties using a number as using square bracket notation will also coerce the argument to string.

console.log(peopleByAge[28]);
// => [{"name":"Alice","age":28}, {"name":"Eve","age":28}]
console.log(peopleByAge["28"]);
// => [{"name":"Alice","age":28}, {"name":"Eve","age":28}]
Enter fullscreen mode Exit fullscreen mode

Grouping with Map.groupBy

Map.groupBy does almost the same thing as Object.groupBy except it returns a Map. This means that you can use all the usual Map functions. It also means that you can return any type of value from the callback function.

const ceo = { name: "Jamie", age: 40, reportsTo: null };
const manager = { name: "Alice", age: 28, reportsTo: ceo };

const people = [
  ceo
  manager,
  { name: "Bob", age: 30, reportsTo: manager },
  { name: "Eve", age: 28, reportsTo: ceo },
];

const peopleByManager = Map.groupBy(people, (person) => person.reportsTo);
Enter fullscreen mode Exit fullscreen mode

In this case, we are grouping people by who they report to. Note that to retrieve items from this Map by an object, the objects have to have the same identity.

peopleByManager.get(ceo);
// => [{ name: "Alice", age: 28, reportsTo: ceo }, { name: "Eve", age: 28, reportsTo: ceo }]
peopleByManager.get({ name: "Jamie", age: 40, reportsTo: null });
// => undefined
Enter fullscreen mode Exit fullscreen mode

In the above example, the second line uses an object that looks like the ceo object, but it is not the same object so it doesn't return anything from the Map. To retrieve items successfully from the Map, make sure you keep a reference to the object you want to use as the key.

When will this be available?

The two groupBy methods are part of a TC39 proposal that is currently at stage 3. This means that there is a good chance it will become a standard and, as such, there are implementations appearing.

Chrome 117 just launched with support for these two methods, Firefox Nightly has implemented them behind a flag. Safari had implemented these methods under different names, I'm sure they will be update that soon. As the methods are in Chrome that means they have been implemented in V8, so will be available in Node the next time V8 is updated.

Why use static methods?

You might wonder why this is being implemented as Object.groupBy and not Array.prototype.groupBy. According to the proposal there is a library that used to monkey patch the Array.prototype with an incompatible groupBy method. When considering new APIs for the web, backwards compatibility is hugely important. This was highligted a few years ago when trying to implement Array.prototype.flatten, in an event known as SmooshGate.

Fortunately, using static methods actually seems better for future extensibility. When the Records and Tuples proposal comes to fruition, we can add a Record.groupBy method for grouping arrays into an immutable record.

JavaScript is filling in the gaps

Grouping items together is clearly an important thing we do as developers. lodash.groupBy is currently downloaded from npm between 1.5 and 2 million times a week. It's great to see JavaScript filling in these gaps and making it easier for us to do our jobs.

For now, go get Chrome 117 and try these new methods out for yourself.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .