Group arrays natively, no Lodash and no reduce()
Anyone who wanted to group JavaScript arrays by a criterion used to reach for Lodash, Underscore, or a nested reduce() chain. With Object.groupBy and Map.groupBy, ES2024 finally brings a native, readable solution directly into the language, with a clear difference between the two that is often overlooked in practice.
Table of Contents
- 1. The problem with manual grouping
- 2. Syntax and the basic concept of Object.groupBy
- 3. Map.groupBy: when the key can't be a string
- 4. Object.groupBy vs. Map.groupBy: the decisive difference
- 5. Practical examples: orders, products, user data
- 6. Comparison with reduce() and Lodash groupBy
- 7. Null prototype: why Object.groupBy doesn't return a normal object
- 8. Iterable support: not just arrays, but also Sets and Maps
- 9. Browser compatibility and polyfill strategy
- 10. Summary
- 11. FAQ
1. The problem with manual grouping
Grouping array elements by a key is one of the most common operations in JavaScript applications. Whether it's orders by status, products by category, or log entries by severity, developers keep landing on the same basic task. For years, Array.prototype.reduce() was the standard answer, and libraries such as Lodash offered a more convenient variant under the name _.groupBy(). Both approaches solve the problem, but they bring unnecessary complexity or external dependencies along with them.
With Object.groupBy and Map.groupBy, the TC39 committee has added two static methods to the JavaScript standard library that close exactly this gap. The methods were developed under the Stage 3 proposal "Array Grouping" and have been natively available since Chrome 117, Firefox 119, and Safari 17.4. The result is code that needs no Lodash, no reduce boilerplate, and no type casting, while remaining fully type-safe when used in TypeScript.
2. Syntax and the basic concept of Object.groupBy
Object.groupBy is a static method on the built-in Object object and takes two arguments: an iterable object (e.g. an array) and a callback function that returns the grouping key as a string or symbol for each element. The result is an object whose keys are the callback's return values and whose values are arrays of the associated elements. The original elements are not copied but taken over by reference, so mutations on the grouped result directly affect the source data.
As with Array.prototype.map(), the callback receives the current element, the index, and the original array as arguments. If the callback returns undefined, the element is placed in the group with the key "undefined", an implicit behavior that can lead to unexpected results in practice. Anyone who wants to be safe should design the callback defensively and explicitly define a fallback key. Object.groupBy uses string conversion internally for non-string keys, which is why objects don't work sensibly as keys.
// Object.groupBy: basic usage with an order list
const orders = [
{ id: 1, status: "pending", total: 49.90 },
{ id: 2, status: "shipped", total: 129.00 },
{ id: 3, status: "pending", total: 79.50 },
{ id: 4, status: "delivered", total: 220.00 },
{ id: 5, status: "shipped", total: 59.90 },
];
// Group by status: callback must return a string or symbol
const byStatus = Object.groupBy(orders, order => order.status);
// Result shape:
// {
// pending: [{ id: 1, … }, { id: 3, … }],
// shipped: [{ id: 2, … }, { id: 5, … }],
// delivered: [{ id: 4, … }]
// }
console.log(byStatus.pending.length); // 2
console.log(byStatus.shipped[0].total); // 129
// Group by price range: computed key via callback
const byRange = Object.groupBy(orders, ({ total }) => {
if (total < 60) return "low";
if (total < 150) return "mid";
return "high";
});
console.log(byRange.low.map(o => o.id)); // [1, 5]
console.log(byRange.high.map(o => o.id)); // [4]
3. Map.groupBy: when the key can't be a string
Map.groupBy works identically to Object.groupBy, but returns a Map instance instead of a plain object. The key difference lies in key handling: a Map can use arbitrary values as keys, objects, arrays, class instances, or primitive values. That opens up use cases that Object.groupBy can't represent, such as grouping by a complex category object rather than its string representation.
Returning a Map also means the iteration order follows insertion order, unlike plain objects, where numeric keys always come first. That's particularly relevant when the order of groups matters in a UI or in ordered output. Access happens via map.get(key) instead of obj[key], which feels different when it comes to typing and use in generic code, but offers more control over key types.
4. Object.groupBy vs. Map.groupBy: the decisive difference
Both methods share the same signature and the same semantics for the callback function, but differ fundamentally in their return type and thus in the key types they allow. Object.groupBy implicitly converts all keys to strings, because JavaScript objects only know string and symbol keys. Map.groupBy returns a genuine Map and treats keys with strict equality using the SameValueZero algorithm, similar to ===, but without the special handling of NaN.
A common pitfall: anyone who returns an object as a key in Object.groupBy ends up with all elements landing in the group [object Object], because JavaScript converts the object to that string. With Map.groupBy, the object itself is used as the key, and each separate object instance produces its own group. That's the reason why Map.groupBy should always be chosen when grouping by domain objects.
| Aspect | Object.groupBy | Map.groupBy |
|---|---|---|
| Return type | Null-prototype object | Map instance |
| Allowed key types | String, symbol (others are converted to string) | Any value, including objects and arrays |
| Accessing groups | result.key or result["key"] |
result.get(key) |
| Iteration order | Numeric keys first, then string order | Strictly by insertion order |
| Ideal for | String keys, JSON output, simple lookups | Object keys, ordered output, complex domain models |
5. Practical examples: orders, products, user data
In an e-commerce application, grouping products by category or orders by delivery status is one of the most common use cases for Object.groupBy. Instead of writing a reduce() function with an initial empty object and defensively initializing every key as before, a single call is enough. The result can be used directly as a data structure for UI components, for example to render products by category in separate list sections or to split orders by status into Kanban columns.
For user data with complex profile objects as the grouping key, such as "group all activities by user object", Map.groupBy is the right choice. The user object key is preserved, and via map.forEach((activities, user) => …) you can access both the group and the full user object without first serializing it into a string key and later deserializing it again.
// Practical example: group products by category object (Map.groupBy)
const electronics = { id: "cat-1", name: "Electronics" };
const clothing = { id: "cat-2", name: "Clothing" };
const products = [
{ name: "Laptop", price: 999, category: electronics },
{ name: "T-Shirt", price: 29, category: clothing },
{ name: "Monitor", price: 349, category: electronics },
{ name: "Jeans", price: 59, category: clothing },
];
// Map.groupBy preserves the object reference as the key
const byCategory = Map.groupBy(products, p => p.category);
byCategory.forEach((items, cat) => {
console.log(`${cat.name}: ${items.map(i => i.name).join(", ")}`);
// Electronics: Laptop, Monitor
// Clothing: T-Shirt, Jeans
});
// Object.groupBy with a derived string key: compute total per status
const orders = [
{ id: 1, status: "pending", total: 49.90 },
{ id: 2, status: "shipped", total: 129.00 },
{ id: 3, status: "pending", total: 79.50 },
];
const grouped = Object.groupBy(orders, o => o.status);
// Derive summary from grouped result
const summary = Object.entries(grouped).map(([status, items]) => ({
status,
count: items.length,
totalRevenue: items.reduce((sum, o) => sum + o.total, 0),
}));
console.log(summary);
// [{ status: "pending", count: 2, totalRevenue: 129.40 }, …]
6. Comparison with reduce() and Lodash groupBy
The classic implementation with reduce() isn't wrong, but it requires three things that Object.groupBy eliminates: the initial empty object as an accumulator, the defensive initialization of the array slot for every new key, and explicitly returning the accumulator at the end of every call. With nested grouping, this boilerplate multiplies and makes the code hard to read. Object.groupBy eliminates all of that down to a single line.
Lodash's _.groupBy() offers the same readability, but brings the entire Lodash library along as a dependency, or at least the grouping module with tree-shaking. In modern projects targeting ES2024, that's no longer necessary. Anyone who included Lodash only for _.groupBy(), _.sortBy(), and similar array helpers can remove the dependency entirely and use native methods instead. That saves bundle size, reduces the need for security updates, and makes the code self-documenting because there's no external API to learn.
7. Null prototype: why Object.groupBy doesn't return a normal object
Object.groupBy returns an object with a null prototype, meaning it has no inherited methods such as toString(), hasOwnProperty(), or constructor. This is a deliberate design decision: because the grouped object could potentially contain keys such as constructor, toString, or __proto__ (if the data carries these values), a normal object with a prototype chain would lead to unexpected collisions. A null-prototype object prevents that entirely.
In practice, this means you can't call methods such as result.hasOwnProperty("pending") on the result of Object.groupBy, that would throw a TypeError. Instead, use Object.hasOwn(result, "pending") or simply check directly with "pending" in result. For serialization with JSON.stringify(), the null-prototype object behaves like a normal object, all own properties are taken into account. So the behavior is correct for the typical use case, but it needs to be known to avoid surprises.
8. Iterable support: not just arrays, but also Sets and Maps
Both Object.groupBy and Map.groupBy accept any iterable as the first argument, not just arrays. That includes Set, Map, String, arguments objects, generator functions, and any class implementing the iterator protocol. That's especially useful when data already exists in another data structure and converting it into an intermediate array should be avoided.
A Set of unique tag strings can be grouped directly with Object.groupBy by first letter or by length, without first calling Array.from(set). A Map can be grouped directly via its entries (map.entries()), with the callback receiving the [key, value] tuple. This flexibility makes Object.groupBy and Map.groupBy universal tools for any iterable source in modern JavaScript development.
// Object.groupBy works on any iterable, not just arrays
const tags = new Set(["JavaScript", "TypeScript", "Java", "CSS", "C++", "JSX"]);
// Group Set elements by first letter, no Array.from() needed
const byLetter = Object.groupBy(tags, tag => tag[0].toUpperCase());
console.log(byLetter["J"]); // ["JavaScript", "Java", "JSX"]
console.log(byLetter["C"]); // ["CSS", "C++"]
// Grouping generator output directly
function* range(start, end) {
for (let i = start; i <= end; i++) yield i;
}
const grouped = Object.groupBy(range(1, 10), n => n % 2 === 0 ? "even" : "odd");
console.log(grouped.even); // [2, 4, 6, 8, 10]
console.log(grouped.odd); // [1, 3, 5, 7, 9]
// TypeScript: typed usage with generics
interface Product { name: string; price: number; category: string; }
function groupProducts(products: Product[]) {
// Return type is inferred: Record<string, Product[]>
return Object.groupBy(products, p => p.category);
}
9. Browser compatibility and polyfill strategy
Object.groupBy and Map.groupBy have been natively supported since Chrome 117 (August 2023), Firefox 119 (October 2023), and Safari 17.4 (March 2024). Node.js supports both methods from version 21 onward. For current projects with a modern browser target (e.g. the last two versions), both methods can be used without a polyfill. Anyone who needs to support older browsers or Node.js 18 needs a polyfill.
A simple polyfill for Object.groupBy is quick to write and doesn't need to be an external dependency: check whether Object.groupBy is already defined, and if not, define it as a function that internally uses reduce(). For production use, however, core-js is recommended as a comprehensive polyfill library that implements both methods correctly and handles edge cases, and can be pulled in automatically via Babel or Vite. With a Browserslist target such as > 0.5%, last 2 years, not dead, the polyfill is no longer needed in most modern projects.
Mironsoft
Modern JavaScript development for scalable web applications
Want to remove Lodash dependencies from your project?
We analyze your JavaScript code for unnecessary dependencies and migrate Lodash calls to native ES2024 methods, for a smaller bundle size and future-proof code.
Code audit
Systematic analysis of Lodash dependencies and native alternatives
Migration
Step-by-step transition to Object.groupBy, Map.groupBy, and other ES2024 APIs
Polyfill setup
Configuring Browserslist and core-js correctly for optimal compatibility
10. Summary
Object.groupBy and Map.groupBy are two of the most useful additions in ES2024 for everyday JavaScript work. They solve the common problem of array grouping natively, without external libraries and without the boilerplate of a reduce() chain. The main difference lies in the return type: Object.groupBy delivers a null-prototype object with string keys and is therefore ideal for simple, JSON-compatible structures. Map.groupBy delivers a genuine Map and allows arbitrary key types, including objects, indispensable for domain models with complex keys.
Both methods accept any iterable, are fully browser-compatible in modern environments, and enable clean TypeScript typing without additional generics. Anyone still importing _.groupBy() from Lodash today should check whether the native variant is sufficient, in most cases it is, and the step toward a Lodash-free codebase starts right here.
Object.groupBy and Map.groupBy, the essentials at a glance
Object.groupBy
Returns a null-prototype object. Keys are strings or symbols. Ideal for JSON-compatible result structures and simple lookups.
Map.groupBy
Returns a Map. Keys can be any value, including objects. Iteration order strictly follows insertion. Ideal for domain models.
Iterable support
Both methods accept any iterable: array, Set, Map, generator, string. No Array.from() preprocessing needed.
Compatibility
Chrome 117+, Firefox 119+, Safari 17.4+, Node.js 21+. For older targets: use a core-js polyfill via Babel or a Vite plugin.