Object.groupBy()
Groups the items of an iterable into a null-prototype object keyed by a callback's return value.
Object.groupBy(items, callbackFn)- Added in JavaScript ES2024
Parameters
| Parameter | Type | Description |
|---|---|---|
$items | Iterable | An iterable, such as an Array, whose elements will be grouped. |
$callbackFn | Function | A function called for each element as callbackFn(element, index); its return value is coerced to a string and used as the group key. |
Return value
ObjectA null-prototype object with one property per group, each holding an array of the elements in that group.
Errors
Throws a TypeError if items is not iterable or callbackFn is not callable.
Object.groupBy() replaces the reduce() boilerplate everyone writes for bucketing a list. It walks the iterable once, calls the callback for each element, and appends that element to the array under the returned key.
The null prototype
The result is created with no prototype, so it has no inherited members at all: no toString, no hasOwnProperty, no constructor. That is deliberate — group keys come from data, and a group literally named "constructor" or "__proto__" must not collide with anything. The cost is that result.hasOwnProperty(k) throws; use Object.hasOwn() instead, and expect a console to print it as [Object: null prototype].
What trips people up
- Keys are coerced to strings. Grouping by a number gives
"1"; grouping by objects collapses them all into"[object Object]". When the key should stay a real value, useMap.groupBy(). - Only groups that actually occur exist. Accessing an absent group yields
undefined, not an empty array — reach for?? []. - Elements are shared by reference with the source; the arrays are new, the items are not copies.
Example
When to use it
- Bucket records by status or category before rendering a grouped list
- Split a batch of results into successes and failures in one pass
- Tally items per key by grouping and then reading each array's length
Frequently asked questions
Why does hasOwnProperty fail on the result of Object.groupBy?
The returned object has a null prototype, so it inherits no methods at all; use Object.hasOwn(result, key) or the in operator instead.
What is the difference between Object.groupBy and Map.groupBy?
Object.groupBy() coerces every key to a string and returns a null-prototype object, while Map.groupBy() keeps keys as their original values, including objects, and returns a Map.
How do I get a count per group instead of the items?
Group first, then map over the entries, for example Object.fromEntries(Object.entries(groups).map(([k, v]) => [k, v.length])).
More examples
Using the index argument and handling absent groups
The callback receives the index as its second argument, which is why the top group carries one in its key. Groups that no element produced simply do not exist, so distinction is undefined rather than an empty array; ?? [] is the usual guard. JSON.stringify() still works on a null-prototype object.
const scores = [88, 42, 95, 61, 77];
const byGrade = Object.groupBy(scores, (score, index) => {
if (score >= 90) return `top (index ${index})`;
return score >= 60 ? "pass" : "fail";
});
console.log(byGrade.pass); // [ 88, 61, 77 ]
console.log(byGrade.fail); // [ 42 ]
console.log(byGrade.distinction); // undefined — no such group
console.log(byGrade.distinction ?? []); // []
console.log(JSON.stringify(byGrade).slice(0, 30));Numeric keys become strings
Object keys are always strings, so the numeric lengths are converted and then sorted into ascending integer order. Map.groupBy() keeps the keys as actual numbers in insertion order, which matters when the key is an object or when you need to compare it without parsing.
const words = ["hi", "tree", "a", "four", "go"];
const byLength = Object.groupBy(words, (w) => w.length);
console.log(Object.keys(byLength)); // [ '1', '2', '4' ] — strings, numeric order
console.log(byLength[2]); // [ 'hi', 'go' ] — index coerced on lookup
const byLengthMap = Map.groupBy(words, (w) => w.length);
console.log([...byLengthMap.keys()]); // [ 2, 4, 1 ] — real numbers, insertion order
console.log(byLengthMap.get(2)); // [ 'hi', 'go' ]
Discussion