Browse by section

Web Design 日本語

Grouping and Non-Mutating Array Methods in JavaScript

Grouping an array is done with Object.groupBy() and Map.groupBy(). Standardised in ES2024 and available in every modern browser since March 2024.

This article was published in 2024 and revised in September 2026. The code originally shown called array.groupBy()—an array method that does not exist. Running it throws a TypeError. It has been corrected, and the ES2023 non-mutating methods have been added.

  • Array.prototype.groupBy() does not exist. The proposal was originally Array.prototype.group(), was withdrawn over web-compatibility concerns, and shipped instead as the static methods Object.groupBy() and Map.groupBy()
  • flatMap() (ES2019) and at() (ES2022) are not 2024 additions

Sponsored

When each method was added

Method Added What it does
flatMap() ES2019 Map and flatten one level in one pass
at() ES2022 Index from the end with negative numbers
findLast() / findLastIndex() ES2023 Search from the end
toSorted() / toReversed() / toSpliced() / with() ES2023 Return a new array instead of mutating
Object.groupBy() / Map.groupBy() ES2024 Group by a key
Array.fromAsync() ES2024 Build an array from an async iterable

Object.groupBy(): grouping by key

Pass the array first and a key function second. Note it is a static method on Object, not a method on the array.

const users = [
  { name: 'Alice',   age: 28 },
  { name: 'Bob',     age: 22 },
  { name: 'Charlie', age: 22 },
  { name: 'David',   age: 28 },
];

// users.groupBy(...) throws a TypeError
const grouped = Object.groupBy(users, (user) => user.age);

console.log(grouped);
/*
{
  22: [{ name: 'Bob', age: 22 }, { name: 'Charlie', age: 22 }],
  28: [{ name: 'Alice', age: 28 }, { name: 'David', age: 28 }]
}
*/

What used to be a reduce() is now one line.

// before
const grouped = users.reduce((acc, user) => {
  (acc[user.age] ||= []).push(user);
  return acc;
}, {});

// ES2024
const grouped = Object.groupBy(users, (user) => user.age);

Three behaviours to know

1. Keys are always strings. Returning a number still produces a string object key.

const grouped = Object.groupBy(users, (user) => user.age);

grouped[22];    // works (coerced to "22")
grouped['22'];  // the same thing

Object.keys(grouped); // ['22', '28'] — strings

2. The returned object has a null prototype. Unlike an object literal, it has no inherited methods.

const grouped = Object.groupBy(users, (u) => u.age);

grouped.hasOwnProperty('22');          // TypeError
Object.hasOwn(grouped, '22');          // true — use this

This is deliberate. With no prototype, a key like "constructor" or "__proto__" cannot cause surprises.

3. The callback receives the index as its second argument.

const chunked = Object.groupBy(items, (item, index) => Math.floor(index / 3));
// splits into groups of three

Sponsored

Map.groupBy(): when keys must not be strings

Use this when you need to keep the key’s type, or use an object as a key. It returns a Map.

const grouped = Map.groupBy(users, (user) => user.age);

grouped.get(22);   // works — still a number
grouped.get('22'); // undefined

for (const [age, list] of grouped) {
  console.log(age, list.length); // 22 2 / 28 2
}

Objects work as keys too.

const teamA = { id: 1, name: 'Team A' };
const teamB = { id: 2, name: 'Team B' };

const members = [
  { name: 'Alice', team: teamA },
  { name: 'Bob',   team: teamB },
  { name: 'Carol', team: teamA },
];

const byTeam = Map.groupBy(members, (m) => m.team);
console.log(byTeam.get(teamA).length); // 2
Object.groupBy() Map.groupBy()
Returns Null-prototype object Map
Key type Coerced to string Preserved
JSON JSON.stringify() works directly Needs conversion
Best for API responses, display formatting Object keys, preserving order

ES2023: methods that do not mutate

These may matter more day to day. sort() and reverse() modify the original array, which causes real bugs with arrays you must not touch—React state, for example.

const scores = [3, 1, 4, 1, 5];

// mutates
scores.sort();                    // scores itself is reordered
const copy = [...scores].sort();  // the old workaround

// returns a new array, original untouched
const sorted   = scores.toSorted((a, b) => a - b); // [1, 1, 3, 4, 5]
const reversed = scores.toReversed();              // [5, 1, 4, 1, 3]
const replaced = scores.with(0, 99);               // [99, 1, 4, 1, 5]
const spliced  = scores.toSpliced(1, 2);           // [3, 1, 5]

console.log(scores); // [3, 1, 4, 1, 5] — unchanged
Mutating Non-mutating
sort() toSorted()
reverse() toReversed()
splice() toSpliced()
arr[i] = v with(i, v)

toSorted() still needs a comparator. Without one it compares as strings, exactly like sort(), so [10, 9, 100] becomes [10, 100, 9]. That has not changed.

Sponsored

findLast(): searching from the end

const logs = [
  { level: 'info',  msg: 'start' },
  { level: 'error', msg: 'connection failed' },
  { level: 'info',  msg: 'retrying' },
  { level: 'error', msg: 'timeout' },
];

// the most recent error
const lastError = logs.findLast((log) => log.level === 'error');
console.log(lastError.msg); // 'timeout'

const index = logs.findLastIndex((log) => log.level === 'error');
console.log(index); // 3

No more reversing the array first.

flatMap(): map and flatten together

An ES2019 feature, but worth covering because it comes up constantly. It flattens the result of map() by one level.

const customers = [
  { name: 'Alice', purchases: ['Book', 'Pen'] },
  { name: 'Bob',   purchases: ['Notebook', 'Pencil'] },
];

const all = customers.flatMap((c) => c.purchases);
console.log(all); // ['Book', 'Pen', 'Notebook', 'Pencil']

A less well-known trick: returning an empty array drops the element, letting you filter and map in one pass.

const input = ['1', 'abc', '3', '', '5'];

// keep only what converts to a number
const numbers = input.flatMap((s) => {
  const n = Number(s);
  return s !== '' && !Number.isNaN(n) ? [n] : [];
});

console.log(numbers); // [1, 3, 5]

It flattens one level only. For deeper nesting use flat(Infinity).

at(): reading from the end

const fruits = ['apple', 'banana', 'cherry', 'date'];

fruits.at(-1);                  // 'date'
fruits[fruits.length - 1];      // the old way

'hello'.at(-1);                 // 'o' — works on strings too

Out-of-range indices return undefined rather than throwing, so at(-1) on an empty array is safe—use optional chaining afterwards.

const last = list.at(-1);
console.log(last?.name ?? 'none');

Putting them together

Group, aggregate, then sort—a very common sequence.

const orders = [
  { id: 1, category: 'book',  price: 1200 },
  { id: 2, category: 'food',  price: 800  },
  { id: 3, category: 'book',  price: 2400 },
  { id: 4, category: 'game',  price: 5800 },
  { id: 5, category: 'food',  price: 450  },
];

const byCategory = Object.groupBy(orders, (o) => o.category);

const summary = Object.entries(byCategory)
  .map(([category, items]) => ({
    category,
    count: items.length,
    total: items.reduce((sum, o) => sum + o.price, 0),
  }))
  .toSorted((a, b) => b.total - a.total);

console.table(summary);
/*
| category | count | total |
| game     | 1     | 5800  |
| book     | 2     | 3600  |
| food     | 2     | 1250  |
*/

Because this uses toSorted(), no intermediate array is mutated—safe to write inside a React render.

Support

Feature Chrome / Edge Firefox Safari Node.js
toSorted() etc. (ES2023) 110 115 16 20
Object.groupBy() (ES2024) 117 119 17.4 21

Both were available across modern browsers by March 2024. No polyfill needed.

Summary

  • array.groupBy() does not exist. Use Object.groupBy(array, fn) or Map.groupBy(array, fn)
  • Object.groupBy() coerces keys to strings and returns a null-prototype object. Use Object.hasOwn()
  • Use Map.groupBy() to preserve key types or use objects as keys
  • ES2023’s toSorted(), toReversed(), toSpliced() and with() do not mutate
  • toSorted() still needs a comparator, or it sorts as strings
  • findLast() searches from the end
  • Returning [] from flatMap() drops the element
  • flatMap() (ES2019) and at() (ES2022) are not new

For async array work see how to use Promise.any(), and for WeakMap see what WeakMap is actually for.