商品を在庫あり・在庫なしで分ける、投稿を日付ごとにまとめる——配列を「キーごとのかたまり」に分ける処理は、Web アプリを作っていると何度も出てきます。これまでは reduce() にオブジェクトを渡して自分で積み上げるのが定番でしたが、Object.groupBy() を使えば同じことが実質1行で書けます。この記事では基本の書き方から、戻り値が少し特殊な「null プロトタイプのオブジェクト」である点、キーが文字列に変換される点、Map.groupBy() との使い分け、実践的なコード例、古い環境向けのフォールバックまで順に解説します。
目次
これまではグループ分けを reduce で書いていた
まずは比較のために、従来の書き方を見てみます。商品の配列を「在庫あり」と「売り切れ」に分けるコードです。
const products = [
{ name: 'Tシャツ', stock: 12 },
{ name: 'パーカー', stock: 0 },
{ name: 'キャップ', stock: 3 },
{ name: 'トートバッグ', stock: 0 },
];
const grouped = products.reduce((acc, product) => {
const key = product.stock > 0 ? 'inStock' : 'soldOut';
if (!acc[key]) {
acc[key] = []; // キーが初めて出てきたら配列を用意する
}
acc[key].push(product);
return acc; // 次の繰り返しへ引き継ぐ
}, {}); // 初期値は空のオブジェクト
やりたいことは「この要素をどのキーに入れるか」を決めることだけなのに、初期値の {}、キーがまだ無いときの配列の準備、push()、そして return acc という定型のコードが毎回ついて回ります。1行でも書き忘れると壊れますし、初めて読む人には「何をしているコードなのか」がすぐには伝わりません。
Object.groupBy なら1行で書ける
同じ処理を Object.groupBy() で書き直すと、こうなります。
const grouped = Object.groupBy(products, (product) => {
return product.stock > 0 ? 'inStock' : 'soldOut';
});
console.log(grouped);
// {
// inStock: [ { name: 'Tシャツ', stock: 12 }, { name: 'キャップ', stock: 3 } ],
// soldOut: [ { name: 'パーカー', stock: 0 }, { name: 'トートバッグ', stock: 0 } ]
// }
console.log(grouped.inStock.length); // 2
console.log(products.length); // 4 ← 元の配列はそのまま
書くのは「どのキーに入れるか」を返す関数だけです。配列を用意する処理も、値を追加する処理も、戻り値を引き継ぐ処理もすべて Object.groupBy() 側がやってくれます。元の配列は変更されず、新しいオブジェクトが返ってくる点も安心して使えるポイントです。各グループの中では、要素は元の配列に並んでいた順序を保ったまま入ります。
Object.groupBy の構文と引数
構文はシンプルで、引数は2つだけです。配列のメソッドではなく Object の静的メソッドなので、products.groupBy(...) ではなく Object.groupBy(products, ...) と書きます。
Object.groupBy(items, callbackFn)
| 引数 | 説明 |
|---|---|
items | グループ分けしたい反復可能オブジェクト(iterable)。配列のほか Set や NodeList なども渡せる |
callbackFn | 要素ごとに呼ばれる関数。その要素を入れるグループのキーを返す |
コールバック関数が受け取る引数は次の2つです。多くの場合は1つ目だけで足りますが、「何番目の要素か」でグループを決めたいときは2つ目も使えます。
| コールバックの引数 | 説明 |
|---|---|
element | いま処理している要素そのもの |
index | その要素が何番目かを表す数値(0 から始まる) |
const letters = ['a', 'b', 'c', 'd', 'e'];
// index を使って偶数番目・奇数番目に振り分ける
const grouped = Object.groupBy(letters, (element, index) => {
return index % 2 === 0 ? 'even' : 'odd';
});
console.log(grouped.even); // ['a', 'c', 'e']
console.log(grouped.odd); // ['b', 'd']
戻り値は、キーごとに配列を持つ新しいオブジェクトです。どのグループにも要素が1つも入らなかった場合、そのキーはそもそも作られません。つまり「空配列が入っている」のではなく「プロパティが存在しない(undefined)」状態になります。この点は後の実践例で扱います。
戻り値は null プロトタイプのオブジェクト
Object.groupBy() が返すのは、見た目こそ普通のオブジェクトですが、Object.prototype を継承していないオブジェクトです。Object.create(null) で作ったものと同じ状態で、プロトタイプが null になっています。
こうなっている理由は、グループのキーがそのままプロパティ名になるからです。もし普通の {} を返す仕様だったら、データ側に 'toString' や 'constructor' といったキーが現れたときに、オブジェクトが元から持っているメンバーと衝突してしまいます。あらかじめ何も継承していないオブジェクトにしておくことで、どんなキーが来ても安全に扱えるようにしているわけです。
hasOwnProperty などのメソッドは呼び出せない
Object.prototype を継承していないということは、そこに用意されているメソッドが一切使えないということです。普段なにげなく呼んでいる hasOwnProperty() や toString() も例外ではありません。
const grouped = Object.groupBy(products, (p) => (p.stock > 0 ? 'inStock' : 'soldOut'));
console.log(Object.getPrototypeOf(grouped)); // null
console.log(grouped.hasOwnProperty); // undefined
console.log(grouped.toString); // undefined
// grouped.hasOwnProperty('inStock');
// → TypeError: grouped.hasOwnProperty is not a function
受け取ったオブジェクトをそのまま既存のユーティリティ関数に渡していると、その中で hasOwnProperty() が使われていて TypeError になる、という形で表面化します。プロパティの有無を調べたいなら、静的メソッドの Object.hasOwn() を使うのが現在の書き方です。
in 演算子や Object.keys は問題なく使える
一方で、in 演算子やブラケット記法、Object の静的メソッドはオブジェクト自身のプロトタイプに依存しないので、これまでどおり使えます。実用上、困る場面はほとんどありません。
console.log('inStock' in grouped); // true
console.log(Object.hasOwn(grouped, 'soldOut')); // true
console.log(Object.keys(grouped)); // ['inStock', 'soldOut']
console.log(grouped['inStock']); // [ {...}, {...} ]
console.log(JSON.stringify(grouped)); // 文字列化もできる
// for...in や Object.entries での繰り返しもそのまま書ける
for (const [key, list] of Object.entries(grouped)) {
console.log(key, list.length);
}
// どうしても普通のオブジェクトが必要なときはコピーする
const plain = { ...grouped };
console.log(plain.hasOwnProperty('inStock')); // true
キーは文字列に変換される
コールバックが返した値は、そのままキーになるのではなくプロパティのキーに変換されてから使われます。数値を返しても、オブジェクトのキーは文字列です。
const users = [
{ name: '田中', age: 28 },
{ name: '鈴木', age: 34 },
{ name: '佐藤', age: 22 },
];
// 年代(20, 30)でグループ分けする
const byDecade = Object.groupBy(users, (user) => Math.floor(user.age / 10) * 10);
console.log(Object.keys(byDecade)); // ['20', '30'] ← 数値ではなく文字列
console.log(byDecade['20'].length); // 2
console.log(byDecade[20].length); // 2(20 が '20' に変換されるので同じ結果)
読み出すときも byDecade[20] のように数値で書けば自動的に文字列へ変換されるため、日常的な使い方で困ることは少ないはずです。ただし Object.keys() で取り出したキーは文字列なので、そのまま数値として計算したいときは Number() で戻す必要があります。
この「必ず変換される」性質で気をつけたいのが、真偽値と undefined です。true を返せばキーは 'true' という文字列になり、条件分岐を書き忘れて何も返さなかった要素は 'undefined' というキーのグループにまとめられます。後者はエラーにならないぶん、見つけにくい不具合になります。
const posts = [
{ title: '公開済みの記事', published: true },
{ title: '下書きの記事', published: false },
];
// 真偽値をそのまま返すと 'true' / 'false' というキーになる
const a = Object.groupBy(posts, (post) => post.published);
console.log(a.true.length); // 1 ← 読みづらい
// 意味の分かる文字列を返したほうが扱いやすい
const b = Object.groupBy(posts, (post) => (post.published ? 'published' : 'draft'));
console.log(b.published.length); // 1
// 値を返し忘れると 'undefined' キーに集まる
const c = Object.groupBy(posts, (post) => {
if (post.published) return 'published'; // false のときは何も返していない
});
console.log(Object.keys(c)); // ['published', 'undefined']
オブジェクトをキーにしたいときは Map.groupBy
キーが文字列に変換されるということは、オブジェクトをキーにしようとすると困ったことになります。オブジェクトを返した場合、変換の結果はどれも '[object Object]' という同じ文字列になり、全部がひとつのグループにまとまってしまうからです。
そういうときのために用意されているのが Map.groupBy() です。引数の形は Object.groupBy() とまったく同じで、違うのは戻り値が Map であることと、コールバックが返した値がそのままキーとして使われることです。
const dev = { name: '開発チーム' };
const sales = { name: '営業チーム' };
const members = [
{ name: '田中', team: dev },
{ name: '鈴木', team: sales },
{ name: '佐藤', team: dev },
];
// Object.groupBy だと全員が同じキーに入ってしまう
const ng = Object.groupBy(members, (member) => member.team);
console.log(Object.keys(ng)); // ['[object Object]']
// Map.groupBy ならオブジェクトのままキーにできる
const byTeam = Map.groupBy(members, (member) => member.team);
console.log(byTeam.get(dev)); // [ { name: '田中', ... }, { name: '佐藤', ... } ]
console.log(byTeam.get(sales)); // [ { name: '鈴木', ... } ]
console.log(byTeam.size); // 2
Map のキーは「同じオブジェクトを指しているか」で判定されます。上のコードで byTeam.get(dev) が取り出せるのは、配列の中の member.team と変数 dev が同じオブジェクトだからです。中身が同じでも { name: '開発チーム' } と新しく書いて渡すと、別のオブジェクトなので取り出せません。
| 比較する点 | Object.groupBy() | Map.groupBy() |
|---|---|---|
| 戻り値 | null プロトタイプのオブジェクト | Map オブジェクト |
| キーの扱い | プロパティのキー(文字列)に変換される | 返した値がそのままキーになる |
| 取り出し方 | grouped.key / grouped['key'] | grouped.get(key) |
| グループ数 | Object.keys(grouped).length | grouped.size |
| JSON への変換 | JSON.stringify() でそのまま扱える | そのままでは変換できない |
| 向いている場面 | 文字列・数値でグループ分けするとき | オブジェクトなど文字列以外をキーにしたいとき |
実践例1:在庫状況で表示を出し分ける
冒頭の在庫の例を、画面への描画まで含めて書いてみます。ここでのポイントは、該当する要素が1つも無いグループはキーごと存在しないことです。grouped.soldOut.map(...) と書くと、売り切れ商品が無いときに undefined に対して map() を呼ぶことになり TypeError になります。??(null 合体演算子)で空配列を用意しておくと安全です。
const products = [
{ name: 'Tシャツ', stock: 12 },
{ name: 'パーカー', stock: 0 },
{ name: 'キャップ', stock: 3 },
];
const grouped = Object.groupBy(products, (product) => {
return product.stock > 0 ? 'inStock' : 'soldOut';
});
// 該当なしのときは undefined になるので、空配列を用意しておく
const inStock = grouped.inStock ?? [];
const soldOut = grouped.soldOut ?? [];
const toListItems = (list) => list.map((p) => `<li>${p.name}</li>`).join('');
document.getElementById('in-stock').innerHTML = toListItems(inStock);
document.getElementById('sold-out').innerHTML = toListItems(soldOut);
console.log(`在庫あり ${inStock.length}件 / 売り切れ ${soldOut.length}件`);
配列を2回 filter() して同じ条件を2か所に書く方法でも同じ結果は得られますが、その場合は配列を2周しますし、条件を変更するときに片方だけ直してしまう危険があります。Object.groupBy() なら、振り分けの条件は1か所にしか書きません。
実践例2:投稿を日付ごとにまとめて一覧表示する
ブログの一覧や通知欄のように「日付の見出しごとに項目を並べる」レイアウトは、グループ分けが最も活きる場面です。日時の文字列から日付部分だけを取り出してキーにします。
const posts = [
{ title: 'Object.groupBy を使ってみた', publishedAt: '2026-09-13T10:00:00' },
{ title: 'CSS の余白設計を見直す', publishedAt: '2026-09-13T18:30:00' },
{ title: 'WordPress のブロック開発メモ', publishedAt: '2026-09-11T09:15:00' },
];
// '2026-09-13T10:00:00' の先頭10文字 = '2026-09-13' をキーにする
const byDate = Object.groupBy(posts, (post) => post.publishedAt.slice(0, 10));
console.log(Object.keys(byDate)); // ['2026-09-13', '2026-09-11']
const html = Object.entries(byDate)
.sort(([a], [b]) => b.localeCompare(a)) // 日付の新しい順に並べ替える
.map(([date, list]) => {
const items = list.map((post) => `<li>${post.title}</li>`).join('');
return `<section><h3>${date}</h3><ul>${items}</ul></section>`;
})
.join('');
document.getElementById('post-list').innerHTML = html;
Object.entries() で「キーと配列」の組に変換すれば、そのまま map() でマークアップを組み立てられます。なお、グループの並び順は元データの登場順に左右されるため、日付順に見せたいなら上のように明示的に sort() をかけてください。日付をそのまま見出しに出すのではなく 2026年9月13日 のように整形したい場合は、キーを作る関数の中で toLocaleDateString() を使って文字列を作ってしまうのも手です。
実践例3:条件で2つのグループに分ける
「合格と不合格」「有効な入力とエラーになった入力」のように、配列を条件で2つに割りたいことがあります。分割代入と組み合わせると、そのまま2つの変数として受け取れて読みやすくなります。既定値を書いておけば、片方が空になったときの undefined 対策も同時に済みます。
const scores = [
{ name: '田中', score: 82 },
{ name: '鈴木', score: 45 },
{ name: '佐藤', score: 91 },
{ name: '高橋', score: 58 },
];
// 分割代入で2つのグループを直接受け取る(空のときは [] になる)
const { passed = [], failed = [] } = Object.groupBy(scores, (student) => {
return student.score >= 60 ? 'passed' : 'failed';
});
console.log(passed.map((s) => s.name)); // ['田中', '佐藤']
console.log(failed.map((s) => s.name)); // ['鈴木', '高橋']
console.log(`合格 ${passed.length}人 / 不合格 ${failed.length}人`);
3つ以上に分けたいときも考え方は同じで、返す文字列を増やすだけです。点数を 'A' / 'B' / 'C' の評価に変換して返せば、そのまま評価ごとのグループになります。条件を関数に切り出しておけば、グループ分けのルールを1か所で管理できます。
古い環境で使えないときの書き方
Object.groupBy() と Map.groupBy() は ECMAScript 2024 で標準に加わった比較的新しい機能です。主要ブラウザーの新しいバージョンや近年の Node.js では利用できますが、少し前の環境では用意されておらず、呼び出すと Object.groupBy is not a function というエラーになります。業務で古いブラウザーや古い Node.js を対象に含めている場合は、実際に動かす環境で対応状況を確認してから使ってください。
対応していない環境も想定するなら、機能があるかどうかを調べて、無ければ reduce() で同じ形のオブジェクトを作る関数を自前で用意しておくと切り替えが楽になります。
export function groupBy(items, callbackFn) {
// 使える環境ではそのまま標準の機能を使う
if (typeof Object.groupBy === 'function') {
return Object.groupBy(items, callbackFn);
}
// 使えない環境では reduce で同じ形を作る
return [...items].reduce((acc, element, index) => {
const key = callbackFn(element, index);
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(element);
return acc;
}, Object.create(null)); // 戻り値の性質も合わせておく
}
初期値を {} ではなく Object.create(null) にしているのは、標準の Object.groupBy() と戻り値の性質をそろえるためです。こうしておけば、「開発環境では動くのに古いブラウザーでだけ挙動が違う」といった食い違いが起きません。Babel などのトランスパイラーは構文を変換するだけで新しいメソッドまでは追加しないので、ビルドしているから大丈夫とは限らない点にも注意してください。まとめて補いたい場合は core-js のようなポリフィルを読み込む方法もあります。
まとめ
Object.groupBy(items, callbackFn) は、配列などの反復可能オブジェクトを、コールバックが返したキーごとの配列にまとめてくれるメソッドです。reduce() で毎回書いていた初期値・配列の準備・push() といった定型のコードが不要になり、「どのキーに入れるか」だけを書けば済むようになります。元の配列は変更されず、グループ内の順序も保たれます。
覚えておきたいのは、戻り値が Object.prototype を継承しないオブジェクトなので hasOwnProperty() が呼べないこと(in や Object.keys() は使えます)、キーが文字列に変換されること、そして該当なしのグループはキーごと存在しないことの3点です。オブジェクトをキーにしたいときは Map.groupBy() を選びます。まだ新しい機能なので、対象環境によっては reduce() によるフォールバックを用意しておくと安心して導入できます。