Object.groupBy and Map.groupBy in JavaScript: Group Data Without Lodash
AI generated
JS
() =>
JavaScript · ES2024 · Array methods
Object.groupBy and Map.groupBy
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.

8 min read Object.groupBy · Map.groupBy · reduce() · Iterable V8 117+ · Firefox 119+ · Safari 17.4+

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.

11. FAQ: Object.groupBy and Map.groupBy in JavaScript

1What is Object.groupBy in JavaScript?
A static ES2024 method that splits an iterable into groups based on a callback function and returns a null-prototype object. Each key corresponds to a callback return value, each value is an array of the associated elements.
2Difference between Object.groupBy and Map.groupBy?
Object.groupBy returns an object (string keys). Map.groupBy returns a Map (any keys, including objects). The choice depends on the key type.
3Can I use Object.groupBy instead of Lodash's _.groupBy?
Yes, in most cases. Both methods solve the same use case without an external dependency. For object keys, use Map.groupBy.
4Why a null prototype?
It prevents collisions with inherited prototype properties such as constructor or toString that could appear as data keys.
5TypeScript support?
From TypeScript 5.4 onward. The return type is inferred as Partial<Record<string, T[]>>. With strict null checks, verify a key exists before accessing it.
6Does it work with Sets and generators?
Yes, both methods accept any iterable. Set, Map, generator functions, custom iterables, no Array.from() intermediate step needed.
7What happens with undefined as a key?
The element ends up in the "undefined" group. No element is lost. Define a fallback value in the callback with ?? to avoid this.
8Reference or copy?
Elements are taken by reference. Mutations on grouped results affect the original data. Create an explicit copy for isolation.
9Browser support?
Chrome 117+, Firefox 119+, Safari 17.4+, Node.js 21+. For older targets: configure a core-js polyfill via Babel or Vite.
10Is nested grouping possible?
Yes. Object.entries(grouped).map(([key, items]) => Object.groupBy(items, …)) creates multi-level hierarchies, for example first by category, then by status.