Collections5 min read

Object.groupBy and Map.groupBy Use Different Kinds of Keys

Choose Object.groupBy or Map.groupBy based on property-key coercion, object identity, symbols, and result access.

  • Object.groupBy
  • Map.groupBy
  • collections

Object.groupBy and Map.groupBy perform the same partitioning step but preserve different keys. Object.groupBy converts each callback result to a property key, so its keys are strings or symbols. Map.groupBy keeps each callback result as a Map key, including object identity.

Use Object.groupBy when the categories already have stable property names and callers want bracket access. Use Map.groupBy when keys are objects, numbers that must remain numbers, or values that should not be coerced into strings.

Both methods became part of ECMAScript 2024 and are present in ECMAScript 2026. Check an older target runtime before shipping them, or provide a tested compatibility layer.

Both methods consume an iterable once

The first argument may be any synchronous iterable, not only an array. The callback receives the current value and a zero-based index. Each input value is appended to one group array, and the values within each group keep their input order.

const readings = [12, 7, 18, 5];

const byRange = Object.groupBy(readings, (value) =>
  value >= 10 ? 'double-digit' : 'single-digit'
);

console.log(byRange['double-digit']); // [12, 18]
console.log(byRange['single-digit']); // [7, 5]

The methods group the original values. They do not clone each item or project it through the callback. Mutating an object reached through a group also mutates that same object in the input collection.

If the callback throws, iteration stops and the method throws. The partial result is not returned. As with other iterable-consuming APIs, a custom iterator gets its normal close handling when completion is abrupt.

Object.groupBy coerces to a property key

The specified Object.groupBy operation applies property-key conversion to callback results. Numbers and booleans become strings. Symbols remain symbols. Distinct values can collapse into the same string key.

const mixed = [1, '1', true, 'true'];
const groups = Object.groupBy(mixed, (value) => value);

console.log(Reflect.ownKeys(groups)); // ['1', 'true']
console.log(groups['1']);            // [1, '1']
console.log(groups.true);            // [true, 'true']

Returning an object usually produces the string key "[object Object]". Custom coercion methods can change that string, but depending on such behavior makes grouping hard to read and can merge unrelated objects. Use Map.groupBy for identity keys.

Symbols are the exception to string coercion:

const open = Symbol('open');
const closed = Symbol('closed');
const jobs = [{ done: false }, { done: true }, { done: false }];

const byState = Object.groupBy(jobs, (job) => job.done ? closed : open);

console.log(byState[open].length);   // 2
console.log(byState[closed].length); // 1
console.log(Object.keys(byState));   // []

Object.keys omits symbol keys, so use Reflect.ownKeys or Object.getOwnPropertySymbols when symbol categories are possible.

The object result has a null prototype

Object.groupBy returns an object whose prototype is null. The design prevents category names such as constructor or toString from colliding with inherited Object.prototype members.

const groups = Object.groupBy(['x', 'y'], () => 'constructor');

console.log(Object.getPrototypeOf(groups)); // null
console.log(groups.constructor);            // ['x', 'y']
console.log(typeof groups.hasOwnProperty);  // undefined
console.log(Object.hasOwn(groups, 'constructor')); // true

Do not call groups.hasOwnProperty(...), because that method is not inherited. Object.hasOwn(groups, key) works. Object spread can copy the enumerable group properties into a conventional object if an API requires Object.prototype, though symbol keys will also be copied.

Serialization is another tradeoff. JSON.stringify handles the result’s enumerable string-keyed properties, but it ignores symbol-keyed groups. Numeric-looking string keys also follow ordinary object key ordering, which can differ from the order in which the categories first appeared.

Map.groupBy preserves key identity

The specified Map.groupBy method keeps the callback result as the map key. That makes it the right choice when category objects already exist.

const paid = { name: 'paid' };
const trial = { name: 'trial' };
const users = [
  { name: 'Ari', plan: paid },
  { name: 'Bea', plan: trial },
  { name: 'Chen', plan: paid },
];

const byPlan = Map.groupBy(users, (user) => user.plan);

console.log(byPlan.get(paid).map((user) => user.name)); // ['Ari', 'Chen']
console.log(byPlan.get({ name: 'paid' }));              // undefined

The final lookup fails because a new object has a new identity. Preserve a reference to the key, or group by a stable primitive identifier instead.

Map keys use SameValueZero. NaN values form one group, and 0 and -0 form one group. String and number keys remain different, so 1 and '1' do not merge as they do with Object.groupBy.

Grouping is shallow and eager

Both methods consume the iterable and allocate an array for every group before returning. They are convenient for finite in-memory input. They are a poor match for an unbounded iterator or a stream whose groups should be processed incrementally.

For a large data set, a hand-written loop can aggregate counts or totals without retaining every source value. It can also cap group sizes, report progress, or spill data elsewhere. groupBy is clearest when the desired result truly is a collection of arrays.

Choose the result type from the key contract. Property names point to Object.groupBy. Identity-bearing or non-coerced keys point to Map.groupBy.