Object.groupBy()

The built-in version of the reduce everyone wrote by hand. Its null prototype is a deliberate safety feature, and the thing most likely to catch you out.

Object static methodES2024Live demo
Common call
Object.groupBy(items, x => x.type)
Returns
a null-prototype object of arrays
Replaces
a reduce that pushes into an accumulator
Watch out
no prototype — hasOwnProperty and toString are absent
Object.groupBy(itemsitems — Any iterable — an array, a Set, a generator.type: iterable · required, callbackcallback — Called as callback(element, index). Its return value becomes the group key, coerced to a string — so returning an object gives you one bucket named "[object Object]".type: Function · required)
→ object

Demo

Live evaluation
Try:
Inputs
itemsnumber[]numbers, comma separated
Output
Object.groupBy([1, 2, 3, 4], n => n % 2 ? 'odd' : 'even')
{ odd: [1, 3], even: [2, 4] }

Each item is passed to the callback and filed under whatever string comes back, preserving the original order within each bucket. A key only appears if something landed in it — the second case has no 'even' property at all, rather than an empty array, so code that reads result.even must handle undefined. An empty input gives an empty object. What the output cannot show is that this object has a null prototype: it has no toString, no hasOwnProperty, and does not inherit anything from Object.prototype.

Parameters

NameTypeRequiredDescription
itemsiterableyesAny iterable — an array, a Set, a generator.
callbackFunctionyesCalled as callback(element, index). Its return value becomes the group key, coerced to a string — so returning an object gives you one bucket named "[object Object]".

Return value

object — An object with a NULL prototype, mapping each key the callback returned to an array of the items that produced it. Insertion order follows first appearance, subject to the usual integer-key rule.

Common patterns

Group records by a field
The everyday use.
const byStatus = Object.groupBy(orders, o => o.status);
Group with a Map for non-string keys
Map.groupBy keeps the key as given.
const byDate = Map.groupBy(events, e => e.date);
Default for a missing bucket
Absent keys are undefined, not empty arrays.
const pending = byStatus.pending ?? [];

Examples

1. Odds and evens
Object.groupBy([1, 2, 3], n => n % 2 ? "odd" : "even")
Returns
{odd: [1, 3], even: [2]}
2. Null prototype
Object.getPrototypeOf(Object.groupBy([1], () => "a"))
Returns
null
3. No toString
String(Object.groupBy([1], () => "a"))
Returns
TypeError: Cannot convert object to primitive value
4. Missing bucket
Object.groupBy([1], () => "odd").even
Returns
undefined
5. Empty input
Object.groupBy([], x => x)
Returns
{}
6. Keys are stringified
Object.groupBy([1], () => 1)
Returns
{'1': [1]}

Pitfalls

1. The result has a null prototype
Deliberate — it means a group key of __proto__ or toString cannot interfere with anything. The cost is that the result has no inherited methods at all, so String(), template literals and hasOwnProperty all fail on it.
No toString
String(Object.groupBy([1], () => "a"))
TypeError: Cannot convert object to primitive value
Give it one
Object.assign({}, Object.groupBy([1], () => "a"))
a normal object
2. Absent groups are undefined, not empty arrays
A bucket exists only if at least one item landed in it. Code that iterates a known set of categories and reads each one directly gets undefined for the empty ones, and then throws on .length or .map.
Throws
Object.groupBy([1], () => "odd").even.length
TypeError: Cannot read properties of undefined (reading 'length')
Default it
(Object.groupBy([1], () => "odd").even ?? []).length
0
3. Keys are coerced to strings
Returning a number gives a string key — and the usual integer-key ordering then applies. Returning an object gives a single bucket called "[object Object]", which silently merges everything into one group.
One bucket
Object.groupBy([1, 2], x => ({id: x}))
{'[object Object]': [1, 2]}
Use Map.groupBy
Map.groupBy([1, 2], x => ({id: x}))
two entries, object keys
4. ES2024 — check your runtime
Node 21+ and 2024-era browsers. The reduce it replaces is three lines and works everywhere, which is still the right fallback for older targets.
Missing
Object.groupBy(xs, f)
TypeError: Object.groupBy is not a function
Reduce
xs.reduce((acc, x) => {
  (acc[f(x)] ??= []).push(x);
  return acc;
}, {})
same shape, normal prototype

When to use

Use it
  • Bucketing records by a string field
  • Partitioning a list into named categories
  • Replacing a hand-written grouping reduce
Reach for something else
  • Keys are not strings → Map.groupBy, which preserves them
  • You need the result to behave like a normal object → spread it into one
  • You want counts rather than the items → reduce, or map over the groups
  • Targeting runtimes older than 2024 → the reduce form

Notes

Complexity
O(n) — one pass, one callback per item
Return
A new null-prototype object whose values are new arrays
CPython impl
V8: Builtins-object-groupby
Memory
Allocates the result object plus one array per group
Thread-safe
Single-threaded; the iterable is consumed once

FAQ

So that a group key can never collide with something inherited. If the callback returns "toString" or "__proto__", a normal object would either shadow a method or — historically — corrupt the prototype. A null-prototype object has nothing to collide with.

Object.getPrototypeOf(Object.groupBy([1], () => "a"));   // null

History

ES2024
Object.groupBy and Map.groupBy added together, after an earlier Array.prototype.group proposal was withdrawn over naming and web-compatibility concerns.