配列を「種類ごと」にまとめるとき、reduce で {} に詰めていく書き方があります。ES2024 で入った Object.groupBy を使うと、同じことを1行で書けます。
const items = [
{ name: "りんご", type: "fruit", price: 120 },
{ name: "にんじん", type: "vegetable", price: 80 },
{ name: "バナナ", type: "fruit", price: 100 },
];
// Before: reduce で {} に詰める
const byReduce = items.reduce((acc, x) => {
(acc[x.type] ??= []).push(x);
return acc;
}, {});
// After: Object.groupBy
const byGroup = Object.groupBy(items, (x) => x.type);
console.log(JSON.stringify(byReduce) === JSON.stringify(byGroup)); // true
中身は同じになりました。では、置き換えると何が変わって、何が変わらないのか。この記事では、Node.js v24.14.1 で実際に動かした結果をもとに整理します。コードは .mjs ファイルに保存して node ファイル名.mjs で実行しています。
Object.groupBy と Map.groupBy は、MDN の互換性データでは、どちらも Chrome 117、Firefox 119、Safari 17.4、Node.js 21.0.0 から使えるとされています。手元の WebKit(Safari と同じエンジン)でも、どちらも呼び出して結果が返りました。
変わること: 戻り値が「プロトタイプのないオブジェクト」になる
Object.groupBy の戻り値を console.log すると、先頭に [Object: null prototype] と出ます。ふつうのオブジェクトが Object.prototype から受け継いでいる hasOwnProperty や toString などのメソッドを、受け継いでいないという意味です。
console.log(byReduce.hasOwnProperty("fruit")); // true
byGroup.hasOwnProperty("fruit");
// TypeError(Node.js での表示): byGroup.hasOwnProperty is not a function
reduce 版で hasOwnProperty を使っていたコードは、置き換えるとエラーになりました。キーがあるかを調べるときは、Object.hasOwn か in を使います。
console.log(Object.hasOwn(byGroup, "fruit")); // true
console.log("fruit" in byGroup); // true
console.log("toString" in byGroup); // false(reduce 版なら true)
文字列の中に埋め込もうとしても、エラーになりました。JSON.stringify は、reduce 版と同じように文字列にできます。
console.log(`${byGroup}`);
// TypeError(Node.js での表示): Cannot convert object to primitive value
プロトタイプがないと何がうれしいのか
reduce 版で、グループのキーが constructor や toString になる場合を試しました。
const tags = [
{ name: "a", tag: "constructor" },
{ name: "b", tag: "toString" },
];
tags.reduce((acc, x) => {
(acc[x.tag] ??= []).push(x);
return acc;
}, {});
// TypeError(Node.js での表示): acc[x.tag].push is not a function
console.log(Object.groupBy(tags, (x) => x.tag));
// [Object: null prototype] {
// constructor: [ { name: 'a', tag: 'constructor' } ],
// toString: [ { name: 'b', tag: 'toString' } ]
// }
1件目の constructor で止まりました(toString でも同じ理由で止まります)。{} から始めた reduce 版では、acc.constructor に最初から Object.prototype から受け継いだ値(Object という関数)が見えているので、??= で空の配列が入らず、push できずに止まりました。プロトタイプのない Object.groupBy の戻り値なら、どんなキーでもぶつかりません。利用者が入力したタグ名やカテゴリ名でグループ分けするときは、この違いが効きます。
変わらないこと(reduce 版でも同じ)
ここから先は、Object.groupBy に特有のくせではなく、reduce 版でも同じように起きることです(キーの性質は、結果をオブジェクトで受け取る以上は避けられません)。置き換えても直りません。
キーは文字列になる
数字や true / false でグループ分けしても、キーは文字列になりました。
const nums = [1, 2, 3, 4];
const g2 = Object.groupBy(nums, (n) => n % 2);
console.log(Object.keys(g2)); // [ '0', '1' ](reduce 版も同じ)
console.log(Object.keys(g2).includes(0)); // false
console.log(Object.keys(g2).includes("0")); // true
g2[0] と書いても、0 が文字列 "0" に変換されて読めるので、気づきにくいところです。キーを取り出して数字として比べると、上のように食い違います。なお、最初に出てくるのは 1 なのに '0' が先に並んでいる理由は、次の項目で説明します。
0以上の整数として読めるキーは、小さい順に並ぶ
新しい年から順に並べたデータをグループ分けすると、キーは小さい順に並び替わりました。
const years = [{ y: 2026 }, { y: 2025 }, { y: 2024 }];
console.log(Object.keys(Object.groupBy(years, (x) => x.y)));
// [ '2024', '2025', '2026' ](reduce 版も同じ)
オブジェクトのキーは、0以上の整数として読めるものが先に小さい順で並び、ほかの文字列は作られた順に並ぶ決まりだからです。-1、1.5、01 のようなキーは並び替わらず、作られた順のままでした。
console.log(Object.keys(Object.groupBy([{ k: 2 }, { k: -1 }, { k: 1.5 }, { k: "01" }, { k: 1 }], (x) => x.k)));
// [ '1', '2', '-1', '1.5', '01' ]
このキーの並び順の決まりは、オブジェクトをループする3つの書き方と、for...in を配列に使ってはいけない理由 でも書きました。
1件もないグループは、キーごと存在しない
console.log(byGroup.meat); // undefined(reduce 版も同じ)
byGroup.meat.length;
// TypeError(Node.js での表示): Cannot read properties of undefined (reading 'length')
全部の種類を「0件」も含めて並べたいときは、先に空の配列を用意して上書きします。
const filled = {
fruit: [],
vegetable: [],
meat: [],
...Object.groupBy(items, (x) => x.type),
};
console.log(filled.meat); // []
filled はふつうのオブジェクトになるので、前の章の hasOwnProperty も使えます(その代わり、プロトタイプのメソッドとキーがぶつかる可能性も戻ります)。
プロパティ名を打ち間違えると、"undefined" というグループになる
const u = Object.groupBy(items, (x) => x.category); // category というプロパティは無い
console.log(Object.keys(u)); // [ 'undefined' ]
エラーにはならず、全部が "undefined" という1つのグループにまとまりました。
中身はコピーではない
byGroup.fruit[0].price = 999;
console.log(items[0].price); // 999
console.log(byGroup.fruit[0] === items[0]); // true
グループの中の要素は、元の配列と同じオブジェクトです(Map.groupBy でも同じでした)。グループ分けしたあとに値を書き換えると、元の配列の値も変わります。
キーの型と並び順を保ちたいなら Map.groupBy
Map.groupBy は、結果を Map で返します。キーの型と、最初に出てきた順が保たれました。
const m = Map.groupBy(items, (x) => x.price >= 100);
console.log([...m.keys()]); // [ true, false ](真偽値のまま)
console.log(m.get(true).length); // 2
console.log(m.get("true")); // undefined(文字列とは区別される)
console.log([...Map.groupBy(years, (x) => x.y).keys()]);
// [ 2026, 2025, 2024 ](元の並び=新しい順のまま)
ただし、並び順は「最初に出てきた順」です。新しい順に表示したいなら、元の配列を先に新しい順に並べておくか、グループ分けのあとで並べ替えます。
種類で分けた Map.groupBy(items, (x) => x.type) でも、1件もない "meat" は get すると undefined でした。また、JSON.stringify(m) は {} になり、中身が出ません。JSON にするなら Object.fromEntries(m) でオブジェクトに戻してから変換します(戻した時点で、キーはまた文字列になります。上の m なら [ 'true', 'false' ])。
オブジェクトをキーにすると、中身ではなく「同じオブジェクトかどうか」で分けられます。
const owner = { id: 1 };
const m2 = Map.groupBy([{ o: owner }, { o: owner }, { o: { id: 1 } }], (x) => x.o);
console.log(m2.size); // 2(中身が同じでも、別のオブジェクトは別のグループ)
使い分けの目安です。
- キーが文字列で、そのまま JSON にしたり、
byGroup.fruitのように読んだりするならObject.groupBy - キーが数字・真偽値・オブジェクトで、型や出てきた順を保ちたいなら
Map.groupBy
AI に置き換えを頼んだときの確認ポイント
- 置き換え前のコードで、戻り値に
hasOwnPropertyを使っていなかったか(Object.hasOwnかinに) - 置き換え前から、キーを数字として比べたり、数字のキーの並び順に頼ったりしていなかったか(置き換えても直らない)
- 0件のグループを、空の配列があるつもりで読んでいないか
-
Map.groupByの結果を、そのままJSON.stringifyしていないか
未経験者向けの講座を運営しています
未経験から Next.js + Supabase + Claude Code で Webアプリを公開するまで を、全24セッションで体系化した教材です。Claude Code を学習パートナーにする CLAUDE.md と学習モード(learner / developer)の設計までセットで含みます。
- ▶ ライブデモ(インストール不要・無料で今すぐ見られます)→ https://trial-web-ayies128s-projects.vercel.app/?ref=qiita
- 教材完全版+月5,500円のメンタリング(全24セッション+チャット質問し放題)→ https://menta.work/plan/20251?ref=qiita
- 無料体験版(git clone して自分の手元で動かす・最初の数セッション分・⭐ Star もよろしくお願いします)→ https://github.com/ayies128/next-ai-camp-trial
- YouTube『AIエンジニア情報局』(AI×開発ニュースを1本5分でキャッチアップできる別運営チャンネル・無料)→ https://www.youtube.com/channel/UC1rXVD9WYsQPQEWZyd-A1KA/?ref=qiita
※ 中上級者には易しすぎる内容なので、初心者の知り合いへの紹介や社内研修の参考としてどうぞ。