The Object.groupBy() static method groups the elements of an iterable into an object. The object’s keys are the group names while the values are arrays containing the elements in each group. This method replaces the Array.prototype.group() instance method that was experimental for some time.

An example

Imagine we have an array of Scotch whisky distilleries. It contains three distilleries for each of the five Scotch whisky regions. We want to group the distilleries by region.

const distilleries = [
{ name: "Glen Scotia", region: "Campbeltown" },
{ name: "Glengyle", region: "Campbeltown" },
{ name: "Springbank", region: "Campbeltown" },

{ name: "Dalmore", region: "Highland" },
{ name: "Glenmorangie", region: "Highland" },
{ name: "Old Pulteney", region: "Highland" },

{ name: "Ardbeg", region: "Islay" },
{ name: "Lagavulin", region: "Islay" },
{ name: "Laphroaig", region: "Islay" },

{ name: "Auchentoshan", region: "Lowland" },
{ name: "Bladnoch", region: "Lowland" },
{ name: "Glenkinchie", region: "Lowland" },

{ name: "Aberlour", region: "Speyside" },
{ name: "Glenfiddich", region: "Speyside" },
{ name: "Glenlivet", region: "Speyside" },
];

To do this, we need the callback function to return the group name for each distillery. In this case, it’s the value of the distillery’s .region property.

const regions = Object.groupBy(distilleries, distillery => distillery.region);

We could use destructuring assignment to shorten this.

const regions = Object.groupBy(distilleries, ({ region }) => region);

Either way, the return value is a null-prototype object. Its keys are the regions while its values are arrays of the distilleries in each region.

{
Campbeltown: [
{ name: "Glen Scotia", region: "Campbeltown" },
{ name: "Glengyle", region: "Campbeltown" },
{ name: "Springbank", region: "Campbeltown" },
],

Highland: [
{ name: "Dalmore", region: "Highland" },
{ name: "Glenmorangie", region: "Highland" },
{ name: "Old Pulteney", region: "Highland" },
],

Islay: [
{ name: "Ardbeg", region: "Islay" },
{ name: "Lagavulin", region: "Islay" },
{ name: "Laphroaig", region: "Islay" },
],

Lowland: [
{ name: "Auchentoshan", region: "Lowland" },
{ name: "Bladnoch", region: "Lowland" },
{ name: "Glenkinchie", region: "Lowland" },
],

Speyside: [
{ name: "Aberlour", region: "Speyside" },
{ name: "Glenfiddich", region: "Speyside" },
{ name: "Glenlivet", region: "Speyside" },
],
}

Iterables

The Object.groupBy() method works with all iterables, not just arrays. I have written about iterables before, but the general idea is that any object can customize its iteration behaviour by implementing the iterable protocol. Syntaxes that work with iterables include spread syntax (...) and for...of loops.

Strings are another type of built-in iterable. For example, we could group a string by uppercase and lowercase letters.

const letters = Object.groupBy("PascalCase", letter => {
const isUppercase = letter === letter.toUpperCase();
return isUppercase ? "upper" : "lower";
});

In this case (pun intended), the return value is a null-prototype object with two properties: .upper and .lower. The value of the former is an array of uppercase letters, while the value of the latter is an array of lowercase letters.

{
upper: ["P", "C"],
lower: ["a", "s", "c", "a", "l", "a", "s", "e"],
}

The Object.groupBy() method also works with custom iterables. For example, let’s use the genAlphaUpper() generator function I wrote about in Generating the alphabet in JavaScript. We could group the letters of the alphabet by those in the A–M range and those in the N–Z range.

const alphabet = Object.groupBy(genAlphaUpper(), letter => {
const LATIN_CAPITAL_LETTER_M = 77;
return letter.codePointAt(0) <= LATIN_CAPITAL_LETTER_M ? "A-M" : "N-Z";
});

Here’s the return value:

{
"A-M": ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M"],
"N-Z": ["N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"],
}

How it works

Here is my own approximation of how the method works. I do not recommend using this as a polyfill because I wrote it for educational purposes only. I have not thoroughly tested it for compliance or browser support. There is a true polyfill in core-js.

function groupBy(items, callbackFn) {
const groups = Object.create(null);
let index = 0;

for (const item of items) {
const group = callbackFn(item, index);

if (Object.hasOwn(groups, group)) {
groups[group].push(item);
} else {
groups[group] = [item];
}

index++;
}

return groups;
}

I start by declaring a function, groupBy(), with two parameters: items and callbackFn. In the function body, I use the Object.create() static method to create a null-prototype object. I assign it to the groups constant. Then I initialize a variable, index, to track the index of the current item in the iterable. This is necessary because for...of loops do not provide the index.

Next, I use a for...of loop to iterate over the iterable object. Inside the loop, I invoke the callbackFn() callback function, passing in the current element and its index. I assign the return value to the group constant. Then I use the Object.hasOwn() static method to check if the object assigned to the groups constant has a property for the current group. If so, I add the current item to that group. Otherwise, I assign the property to a new array containing the current item. At the end of the loop, I increment the value of index. Finally, I return the object assigned to the groups constant.

Non-string keys

If you need the keys to be values other than strings or symbols, you can use the Map.groupBy() static method. It replaces the Array.prototype.groupToMap() instance method that was experimental for some time.

Browser support

The Object.groupBy() method is brand new at the time of writing. It is available in version 117 of Chrome and Edge. It will be available in version 119 of Firefox. It is unsupported in Node.js. You can follow the browser support on Can I Use.