Object.groupBy()
Object.groupBy(items, callbackFn) Object.groupBy() takes an iterable and a callback, runs the callback once per element, and collects the elements into a plain-looking object whose keys come from the callback’s return value. It landed in ECMAScript 2024 as part of the Array Grouping proposal (TC39 Stage 4, November 2023) and shows up in Chrome 117, Firefox 119, Safari 17.4, and Node 21.0.0.
Syntax
Object.groupBy(items, callbackFn)
Parameters
| Name | Type | Description |
|---|---|---|
items | Iterable | The elements to group. Any iterable works (arrays, Sets, generators) because the spec uses iterator semantics, not array indexing. |
callbackFn | (element, index) => string | symbol | Called once per element. The return value becomes the property key. index is the 0-based position of element within items. |
Return value
A null-prototype object (built on Object.create(null)). Each own property is an Array of the elements that mapped to that key. Keys appear in the order the callback first produced them, and elements within a key keep their original order. Empty input returns an empty null-prototype object with no own keys.
The result shares element references with items rather than copying them, so Array.prototype.filter / Array.prototype.map semantics apply. Mutating an element in the result also mutates it in the source.
Group by a property
The typical use case is bucketing records by a single field. Destructuring the element in the callback keeps the call site short.
const inventory = [
{ name: "asparagus", type: "vegetables", quantity: 5 },
{ name: "bananas", type: "fruit", quantity: 0 },
{ name: "goat", type: "meat", quantity: 23 },
{ name: "cherries", type: "fruit", quantity: 5 },
{ name: "fish", type: "meat", quantity: 22 },
];
const byType = Object.groupBy(inventory, ({ type }) => type);
// byType.vegetables
// value: [{ name: 'asparagus', type: 'vegetables', quantity: 5 }]
// byType.fruit.length
// value: 2
// byType.meat[0].name
// value: 'goat'
Group by a computed value
The callback can run any logic, so computed buckets work without a second pass. A ternary or any expression that returns a primitive is enough; the engine builds the buckets in a single pass over the input. This replaces the typical pre-2024 pattern of starting with an empty object and pushing into keys inside a forEach.
const restock = Object.groupBy(
inventory,
({ quantity }) => (quantity > 5 ? "ok" : "restock"),
);
// Object.keys(restock)
// value: ['restock', 'ok']
// restock.ok.map(i => i.name)
// value: ['goat', 'fish']
Gotcha: returning a non-string key collapses everything
The callback’s return value is string-coerced when it isn’t already a string or symbol. Returning a plain object silently buckets every element under "[object Object]".
const broken = Object.groupBy([1, 2, 3], (n) => ({ value: n }));
// Object.keys(broken)
// value: ['[object Object]']
const fixed = Object.groupBy([1, 2, 3], (n) => `v${n}`);
// Object.keys(fixed)
// value: ['v1', 'v2', 'v3']
Pick a string template or a primitive field, not a constructed object.
Gotcha: hasOwnProperty throws
Because the result is null-prototype, the inherited Object.prototype methods are gone. The classic hasOwnProperty call throws.
const groups = Object.groupBy(inventory, ({ type }) => type);
groups.hasOwnProperty("fruit");
// value: TypeError: groups.hasOwnProperty is not a function
Object.hasOwn(groups, "fruit");
// value: true
Use Object.hasOwn() (ES2022) or Object.keys(groups) for enumeration. for...in still works.
Any iterable, not just arrays
items does not have to be an Array. Sets, generators, and other iterables all work, because the method walks the input through the standard iterator protocol. That is also why the index passed to the callback reflects the iteration order, not necessarily a numeric array position when the source is a Set or a generator.
const set = new Set([10, 20, 21, 30]);
const parity = Object.groupBy(set, (n) => (n % 2 === 0 ? "even" : "odd"));
// parity.even
// value: [10, 20, 30]
// parity.odd
// value: [21]
Multi-key composite
When one field is not unique, build a composite key with a template literal. The separator should be something that cannot appear inside any individual field, otherwise two distinct values can hash to the same bucket and the grouping becomes lossy without any error to flag it.
const people = [
{ first: "Ada", last: "Lovelace", age: 36 },
{ first: "Ada", last: "Wong", age: 28 },
{ first: "Alan", last: "Turing", age: 41 },
];
const byName = Object.groupBy(
people,
({ first, last }) => `${first}::${last}`,
);
// Object.keys(byName)
// value: ['Ada::Lovelace', 'Ada::Wong', 'Alan::Turing']
Object.groupBy vs Map.groupBy
Map.groupBy(items, callbackFn) ships in the same proposal with the same callback signature. The differences matter when picking between them.
| Feature | Object.groupBy | Map.groupBy |
|---|---|---|
| Result type | null-prototype object | Map instance |
| Key types | string / symbol (others coerced) | any value, including object references |
| Object identity preserved in key | no (coerced) | yes |
| JSON-serializable result | yes | no (Map becomes {} in JSON.stringify) |
| O(1) lookup | obj.k | map.get(k) |
Reach for Object.groupBy when keys are primitives and the result will travel over the wire or into a database. Reach for Map.groupBy when keys are objects or you need Map-specific operations (size, iteration as [k, v] pairs, or large entry counts).
A pre-ES2024 fallback
For older runtimes, a reduce or a small loop does the same work. The loop is usually faster and easier to read.
function groupBy(items, fn) {
const out = Object.create(null);
let i = 0;
for (const el of items) {
const key = String(fn(el, i++));
(out[key] ??= []).push(el);
}
return out;
}
The ??= and String() bits match the spec’s coercion rule. Drop the String() if you intentionally want to skip non-string keys.
See Also
Object: the constructor that ownsObject.groupBy().Object.hasOwn(): the safe way to check a key on a null-prototype result.Array.prototype.reduce(): the most common pre-ES2024 way to build the same structure.Object.fromEntries(): the inverse transform, handy for reshaping the result.