配列のグループ化は Object.groupBy()Map.groupBy() で書きます。ES2024で標準化され、2024年3月から全モダンブラウザで使えます。

この記事は2024年の公開後、2026年9月に見直しました。 公開当時のコードで使っていた array.groupBy() という配列のメソッドは存在しません。実行すると TypeError になります。正しい構文に差し替えたうえで、ES2023で追加された「元の配列を壊さない」メソッド群も追記しています。

  • Array.prototype.groupBy() は存在しません。 提案段階では Array.prototype.group() という名前でしたが、Web互換性の問題で取り下げられ、静的メソッドの Object.groupBy() / Map.groupBy() として標準化されました
  • flatMap()(ES2019)と at()(ES2022)は2024年の新機能ではありません

いつ追加されたのかを整理する

メソッド 追加 できること
flatMap() ES2019 変換と1段階の平坦化を同時に行う
at() ES2022 負のインデックスで末尾から取得
findLast() / findLastIndex() ES2023 末尾から探す
toSorted() / toReversed() / toSpliced() / with() ES2023 元の配列を壊さずに新しい配列を返す
Object.groupBy() / Map.groupBy() ES2024 キーごとにグループ化する
Array.fromAsync() ES2024 非同期イテラブルから配列を作る

 

Object.groupBy():配列をキーごとにまとめる

第1引数に配列、第2引数にキーを返す関数を渡します。 配列のメソッドではなく、Object の静的メソッドである点に注意してください。

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

// users.groupBy(...) は 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 }]
}
*/

 

これまで reduce() で書いていた処理が1行になります。

// 従来の書き方
const grouped = users.reduce((acc, user) => {
  (acc[user.age] ||= []).push(user);
  return acc;
}, {});

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

 

知っておくべき3つの挙動

1. キーは必ず文字列になります。 数値を返しても、オブジェクトのキーとして文字列化されます。

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

grouped[22];    // 取れる("22" に変換されて一致する)
grouped['22'];  // 同じもの

Object.keys(grouped); // ['22', '28'] ← 文字列

 

2. 戻り値のプロトタイプは null です。 通常のオブジェクトリテラルと違い、hasOwnProperty などのメソッドを持ちません。

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

grouped.hasOwnProperty('22');          // TypeError
Object.hasOwn(grouped, '22');          // true ← こちらを使う

 

これは意図的な設計です。プロトタイプが無いおかげで、"constructor""__proto__" のような値がキーになっても事故が起きません。

3. コールバックの第2引数にインデックスが渡ります。

const chunked = Object.groupBy(items, (item, index) => Math.floor(index / 3));
// 3件ずつのまとまりに分割できる

 

Map.groupBy():キーに文字列以外を使いたいとき

キーをオブジェクトや数値のまま保ちたい場合は Map.groupBy() を使います。戻り値は Map です。

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

grouped.get(22);   // 数値のまま引ける
grouped.get('22'); // undefined

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

 

オブジェクトそのものをキーにすることもできます。

const teamA = { id: 1, name: 'チームA' };
const teamB = { id: 2, name: 'チーム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()
戻り値 プロトタイプなしのオブジェクト Map
キーの型 文字列に変換される そのまま保たれる
JSON化 そのまま JSON.stringify() できる 変換が必要
向く場面 APIレスポンス、表示用の整形 オブジェクトをキーにしたい、順序を保ちたい

 

ES2023:元の配列を壊さないメソッド

実務で効き目が大きいのはこちらかもしれません。 sort()reverse() は元の配列を書き換えてしまうため、Reactの状態など「変更してはいけない配列」を扱うときに事故の原因になります。

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

// 元の配列が書き換わる
scores.sort();          // scores 自体が並び替わる
const copy = [...scores].sort();  // これまでの回避策

// 元の配列はそのまま、新しい配列が返る
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] ← 変わっていない

 

破壊的(元を変える) 非破壊(新しい配列を返す)
sort() toSorted()
reverse() toReversed()
splice() toSpliced()
arr[i] = v with(i, v)

toSorted() にも比較関数は必要です。 省略すると sort() と同じく文字列として比較されるため、[10, 9, 100][10, 100, 9] になります。ここは変わっていません。

 

findLast():末尾から探す

find() の逆方向です。reverse() してから探す必要がなくなりました。

const logs = [
  { level: 'info',  msg: '開始' },
  { level: 'error', msg: '接続失敗' },
  { level: 'info',  msg: '再試行' },
  { level: 'error', msg: 'タイムアウト' },
];

// 最後に起きたエラーを取る
const lastError = logs.findLast((log) => log.level === 'error');
console.log(lastError.msg); // 'タイムアウト'

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

 

flatMap():変換と平坦化を同時に行う

ES2019の機能ですが、使用頻度が高いので整理しておきます。map() の結果を1段階だけ平坦化します

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']

 

知られていない使い方として、空配列を返すことで「その要素を捨てる」ことができます。filter()map() を1回で済ませられます。

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

// 数値に変換できるものだけを数値にして残す
const numbers = input.flatMap((s) => {
  const n = Number(s);
  return s !== '' && !Number.isNaN(n) ? [n] : [];
});

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

 

平坦化されるのは1段階だけです。それ以上の入れ子には flat(Infinity) を使ってください。

 

at():末尾から要素を取る

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

fruits.at(-1);                  // 'date'
fruits[fruits.length - 1];      // 従来の書き方

'hello'.at(-1);                 // 'o' ← 文字列にも使える

 

存在しないインデックスでは undefined が返ります。 空配列に at(-1) を呼んでもエラーにはならないので、その後の処理でオプショナルチェーンを使ってください。

const last = list.at(-1);
console.log(last?.name ?? '該当なし');

 

実践:組み合わせて使う

グループ化してから集計し、並び替える——という処理は頻出です。

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  |
*/

 

toSorted() を使っているので、途中で作った配列を壊しません。Reactの描画処理の中でそのまま書いても安全です。

 

対応状況

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

いずれも2024年3月には全モダンブラウザで使える状態になっており、ポリフィルは不要です。

 

まとめ

  • array.groupBy() は存在しません。 Object.groupBy(array, fn)Map.groupBy(array, fn)
  • Object.groupBy()キーは文字列化され、戻り値はプロトタイプなしObject.hasOwn() で判定する
  • キーの型を保ちたい、オブジェクトをキーにしたいなら Map.groupBy()
  • ES2023の toSorted() / toReversed() / toSpliced() / with() は元の配列を壊しません
  • toSorted() でも比較関数は必要(省略すると文字列比較)
  • findLast() で末尾から探せる
  • flatMap() で空配列を返すと、その要素を捨てられる
  • flatMap()(ES2019)と at()(ES2022)は新機能ではない

非同期処理の配列操作についてはPromise.any() の使い方WeakMap の使いどころはJavaScriptのWeakMapの機能と活用例にまとめています。

ABOUT ME
りん
このブログでは、Web開発やプログラミングに関する情報を中心に、私が日々感じたことや学んだことをシェアしています。技術と生活の両方を楽しめるブログを目指して、日常で触れた出来事や本、グルメの話題も取り入れています。気軽に覗いて、少しでも役立つ情報や楽しいひとときを見つけてもらえたら嬉しいです。