jsguides

Map.groupBy()

Map.groupBy(items, callbackFn)

Map.groupBy() takes an iterable and a callback, then returns a new Map whose keys are the values produced by that callback and whose values are the elements that mapped to each key. It landed in ES2024 and is the native replacement for the “build a dictionary of arrays” pattern that used to require Array.prototype.reduce().

The method is static: call it on Map, not on a Map instance. The returned Map is a fresh collection; the source iterable is not mutated, and the elements inside it are stored by reference, never deep-copied.

Syntax

Map.groupBy(items, callbackFn)
  • items — Any iterable, typically an array, whose elements will be grouped. Set and Map iterables, generators, and the arguments object all work.
  • callbackFn — A function called once per element with (element, index). It should return the value to use as the group key. Any value is allowed, including objects, null, and undefined.

Returns a new Map. Each key is a value returned by callbackFn; each value is an array of source elements that produced that key. Insertion order inside each array matches the source order.

Group strings by length

The simplest case: hand the method an array of strings and a callback that returns a derived property. The result behaves like a dictionary you can read with .get(key).

const words = ["one", "two", "three", "four", "five"];

const byLength = Map.groupBy(words, (word) => word.length);

console.log(byLength.get(3)); // ["one", "two"]
console.log(byLength.get(4)); // ["three", "four", "five"]
console.log(byLength.get(5)); // ["five"]

If two callbacks return the same key, the corresponding elements end up in the same array.

Group objects by a derived value

This is the canonical use case: a list of records that you want to bucket by some field. The example below mirrors the MDN inventory sample and is the cleanest demonstration of why you’d reach for Map.groupBy over Object.groupBy when the key is an object literal.

const inventory = [
  { name: "asparagus", type: "vegetables", quantity: 9 },
  { name: "bananas",   type: "fruit",      quantity: 5 },
  { name: "goat",      type: "meat",       quantity: 23 },
  { name: "cherries",  type: "fruit",      quantity: 12 },
  { name: "fish",      type: "meat",       quantity: 22 },
];

const restock = { restock: true };
const sufficient = { restock: false };

const result = Map.groupBy(inventory, ({ quantity }) =>
  quantity < 6 ? restock : sufficient,
);

console.log(result.get(restock));
// [{ name: "bananas", type: "fruit", quantity: 5 }]

console.log(result.get(sufficient));
// [
//   { name: "asparagus", type: "vegetables", quantity: 9 },
//   { name: "goat",      type: "meat",       quantity: 23 },
//   { name: "cherries",  type: "fruit",      quantity: 12 },
//   { name: "fish",      type: "meat",       quantity: 22 },
// ]

The index argument is available as the second callback parameter, which is handy when the key depends on position. The example below uses it to split elements into even and odd index groups, a common pattern for alternating layouts and partition logic:

const result = Map.groupBy(
  ["a", "b", "c", "d"],
  (_, i) => (i % 2 === 0 ? "even" : "odd"),
);
console.log(result.get("even")); // ["a", "c"]
console.log(result.get("odd"));  // ["b", "d"]

How keys are matched

Map uses reference equality for non-primitive keys, not structural equality. A new object with the same shape will not match an existing key, which is the main reason to use Map.groupBy when the key is itself an object: you can hand the same reference to .get() and it just works.

const restock2 = { restock: true };
console.log(result.get(restock2)); // undefined

Keys are mutable. If you mutate a key after it was inserted, the entry is still found by .get(), because the Map tracks the reference rather than the contents. Mutating the key changes the label you’d see during iteration but does not split the group into a new entry.

undefined, null, and NaN are all valid keys. Map normalizes NaN so every NaN key collapses into a single entry, even though NaN === NaN is false.

Browser and runtime support

Map.groupBy shipped in:

  • Chrome 117 and Edge 117 (September 2023)
  • Firefox 119 (October 2023)
  • Safari 17.4 and iOS Safari 17.4 (March 2024)
  • Node.js 21.0.0

It is part of ES2024. If you need to support iOS Safari 16.4 through 17.3, older Node, or any environment that predates the spec landing, fall back to a polyfill or a manual implementation.

Polyfill

The polyfill is a small loop: walk the iterable, call the callback, and accumulate arrays in a plain Map. Use it as a fallback when Map.groupBy is missing.

function groupByPolyfill(items, callbackFn) {
  const result = new Map();
  for (const [index, element] of items.entries()) {
    const key = callbackFn(element, index);
    if (!result.has(key)) result.set(key, []);
    result.get(key).push(element);
  }
  return result;
}

if (!Map.groupBy) {
  Map.groupBy = groupByPolyfill;
}

For production you can also use the map.groupby package from es-shims or core-js. Both track the spec and run the official test262 suite.

Map.groupBy vs Object.groupBy

Both methods do the same shape of work; the difference is the container.

AspectMap.groupByObject.groupBy
Return typeMap instancePlain object
Key typesAny value: objects, primitives, undefined, NaNStrings and Symbols; other keys are coerced
size availableresult.sizeObject.keys(result).length
IterationInsertion order, directly iterableOwn enumerable string keys, insertion order for strings only
Prototype pollutionNoneTheoretical risk from __proto__ collisions
Best forNon-string keys, dynamic group counts, large result setsJSON-friendly output, string-only keys

Reach for Map.groupBy whenever the key is an object, the group count is large or unknown, or you need .size. Reach for Object.groupBy when you need a JSON-serializable result or you are grouping by a primitive string.

See also