何をしたいユーティリティか:「配列のグループ化」
ここでの「グループ化」は、配列の要素を「ある条件(キー)」ごとにまとめ直す処理です。
SQL の GROUP BY や、Excel の「ピボットテーブルのグループ化」に近いイメージです。
例えば、商品一覧を「カテゴリごと」にまとめたい。
ユーザー一覧を「都道府県ごと」にまとめたい。
ログを「レベル(info / warn / error)ごと」にまとめたい。
こういうときに毎回 for 文を書くのではなく、「グループ化ユーティリティ」として関数を用意しておくと、業務コードがかなり読みやすくなります。
まずはイメージ:グループ化すると何がうれしいか
配列を「キー → 配列」の形に変える
例えば、こんな商品配列があるとします。
const products = [
{ id: 1, name: "Laptop", category: "Electronics" },
{ id: 2, name: "Shirt", category: "Clothing" },
{ id: 3, name: "Phone", category: "Electronics" },
{ id: 4, name: "Pants", category: "Clothing" },
];
JavaScriptこれを「category ごと」にグループ化したいとします。
欲しい形はこうです。
{
Electronics: [
{ id: 1, name: "Laptop", category: "Electronics" },
{ id: 3, name: "Phone", category: "Electronics" },
],
Clothing: [
{ id: 2, name: "Shirt", category: "Clothing" },
{ id: 4, name: "Pants", category: "Clothing" },
],
}
JavaScriptつまり、
「キー(ここでは category の値)」 → 「そのキーに属する要素の配列」
という形に変換するのが「グループ化」です。
この形にしておくと、「カテゴリごとの件数」「カテゴリごとの合計金額」などを計算しやすくなります。
reduce で書く「基本の groupBy」
シンプルな groupBy 実装
まずは、reduce を使った、よくある実装から見てみます。
function groupBy(array, key) {
if (!Array.isArray(array)) {
return {};
}
return array.reduce((groups, item) => {
if (!item || typeof item !== "object") {
return groups;
}
const groupKey = item[key];
const safeKey = String(groupKey);
if (!Object.prototype.hasOwnProperty.call(groups, safeKey)) {
groups[safeKey] = [];
}
groups[safeKey].push(item);
return groups;
}, {});
}
JavaScript重要なポイントをかみ砕いて説明する
reduce の第 2 引数 {} が、「グループをためていく入れ物(オブジェクト)」の初期値です。groups が「今までのグループ結果」、item が「今見ている要素」です。
groupKey は、「この要素が属するグループ名」です。
ここでは単純に item[key] を使っていますが、あとで「関数で決める版」もやります。
safeKey で String(groupKey) としているのは、undefined や数値が来ても、とりあえず文字列キーにして扱えるようにするためです。
groups[safeKey] がまだなければ空配列で初期化し、
その配列に item を push していきます。
最後に groups を返せば、「キー → 配列」の形が完成します。
実際の動き
const products = [
{ id: 1, name: "Laptop", category: "Electronics" },
{ id: 2, name: "Shirt", category: "Clothing" },
{ id: 3, name: "Phone", category: "Electronics" },
{ id: 4, name: "Pants", category: "Clothing" },
];
const grouped = groupBy(products, "category");
console.log(grouped);
/*
{
Electronics: [
{ id: 1, name: "Laptop", category: "Electronics" },
{ id: 3, name: "Phone", category: "Electronics" },
],
Clothing: [
{ id: 2, name: "Shirt", category: "Clothing" },
{ id: 4, name: "Pants", category: "Clothing" },
],
}
*/
JavaScriptES2024 の Object.groupBy を使う(対応ブラウザなら最強)
Object.groupBy の基本
新しめの JavaScript には、ネイティブの Object.groupBy があります。
「配列をグループ化するための専用メソッド」です。【MDN / W3Schools など】
基本形はこうです。
const grouped = Object.groupBy(items, (item) => {
return /* グループ名(文字列) */;
});
JavaScriptコールバック関数が返した値が「グループ名」になり、
同じ値を返した要素が同じグループに入ります。
先ほどの例を Object.groupBy で書き直す
const products = [
{ id: 1, name: "Laptop", category: "Electronics" },
{ id: 2, name: "Shirt", category: "Clothing" },
{ id: 3, name: "Phone", category: "Electronics" },
{ id: 4, name: "Pants", category: "Clothing" },
];
const grouped = Object.groupBy(products, (item) => item.category);
console.log(grouped);
JavaScriptやっていることは groupBy(products, "category") と同じですが、Object.groupBy を使うと「グループ化している」ことが一目で分かり、コードがかなり短くなります。【MDN / Qiita 記事など】
条件でグループ分けする例
例えば、在庫を「在庫あり」「要補充」で分けたい場合。
const inventory = [
{ name: "asparagus", type: "vegetable", quantity: 5 },
{ name: "bananas", type: "fruit", quantity: 0 },
{ name: "cherries", type: "fruit", quantity: 5 },
{ name: "broccoli", type: "vegetable", quantity: 2 },
];
const grouped = Object.groupBy(inventory, (item) => {
return item.quantity > 0 ? "inStock" : "outOfStock";
});
console.log(grouped);
/*
{
inStock: [
{ name: "asparagus", ... },
{ name: "cherries", ... },
{ name: "broccoli", ... },
],
outOfStock: [
{ name: "bananas", ... },
],
}
*/
JavaScript「どのプロパティでグループ化するか」だけでなく、
「どんな条件でグループ名を決めるか」を自由に書けるのが強みです。
関数でグループキーを決める汎用 groupBy
「キー名」ではなく「関数」で柔軟に書く
groupBy(array, key) だと「単純なプロパティ名」でしかグループ化できません。
より柔軟にするには、「要素を受け取ってグループ名を返す関数」を渡せるようにします。
function groupByFn(array, keyFn) {
if (!Array.isArray(array)) {
return {};
}
return array.reduce((groups, item, index) => {
const key = keyFn(item, index);
const safeKey = String(key);
if (!Object.prototype.hasOwnProperty.call(groups, safeKey)) {
groups[safeKey] = [];
}
groups[safeKey].push(item);
return groups;
}, {});
}
JavaScriptkeyFn は、「要素(とインデックス)からグループ名を決める関数」です。Object.groupBy とほぼ同じインターフェースだと思ってください。
実際の使い方
年齢で年代ごとにグループ化する例です。
const users = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 35 },
{ name: "Charlie", age: 28 },
{ name: "Diana", age: 42 },
];
const grouped = groupByFn(users, (user) => {
if (user.age < 30) return "20代";
if (user.age < 40) return "30代";
return "40代以上";
});
console.log(grouped);
/*
{
"20代": [
{ name: "Alice", age: 25 },
{ name: "Charlie", age: 28 },
],
"30代": [
{ name: "Bob", age: 35 },
],
"40代以上": [
{ name: "Diana", age: 42 },
],
}
*/
JavaScriptこのように、「グループ名をどう決めるか」を関数に閉じ込めておくと、
ユーティリティ側は「ひたすらグループに突っ込むだけ」で済みます。
実務で意識してほしい設計のポイント
返り値の形を「オブジェクト」にするか「Map」にするか
ここまでの例はすべて「普通のオブジェクト {}」を返しています。
これは扱いやすい一方で、「キーが何でもアリ」な場合には少し不便です。
ES2024 には Map.groupBy もあり、「キーに任意の値を使える」グループ化もできます。【MDN】
ただし、業務でよくある「文字列キーでグループ化」なら、まずはオブジェクトで十分です。
「キーが文字列で表現できるかどうか」で使い分ける、という考え方を覚えておくとよいです。
「グループ化したあとに何をしたいか」までセットで考える
グループ化はゴールではなく、「集計や表示のための前処理」です。
例えば、グループ化したあとに次のようなことをしたくなります。
各グループの件数を数える。
各グループの合計・平均を出す。
各グループの最大・最小を出す。
このとき、「グループ化ユーティリティ」と「合計・平均・最大・最小ユーティリティ」を組み合わせると、
SQL の GROUP BY + SUM / AVG / MAX / MIN のようなことが、JavaScript だけで自然に書けるようになります。
「キーが存在しない」「値が不正」のときの扱い
現実のデータでは、category が null だったり、空文字だったり、そもそもプロパティがなかったりします。
方針としては、例えば次のように決めておくとよいです。
キーがない・空・不正 → "__unknown__" のような特別なグループにまとめる。
または、そういう要素はグループ化の対象から外す。
どちらにするかは要件次第ですが、ユーティリティの中で「どう扱うか」を一度決めてしまうと、
呼び出し側のコードがスッキリします。
少し手を動かして感覚をつかむ
ブラウザのコンソールや Node.js で、次のようなコードを実際に打ってみてください。
const products = [
{ id: 1, name: "Laptop", category: "Electronics" },
{ id: 2, name: "Shirt", category: "Clothing" },
{ id: 3, name: "Phone", category: "Electronics" },
{ id: 4, name: "Pants", category: "Clothing" },
];
groupBy(products, "category");
groupByFn(products, (p) => p.category);
Object.groupBy(products, (p) => p.category);
JavaScriptそれぞれの結果を見比べて、
「どれも同じような形になっていること」
「書き方の違い(key 版と関数版、ネイティブ版)」
を自分の目で確認してみてください。
そのうえで、自分のプロジェクトに
export function groupBy(...) { ... }
export function groupByFn(...) { ... }
// 対応ブラウザなら Object.groupBy を優先して使う
JavaScriptのようなユーティリティを置き、
「配列をグループ化したくなったら、必ずこの“グループ化ユーティリティ”を通す」
というルールを作ってみてください。
そうすると、あなたのコードは、「その場しのぎの for 文」から、「意図がはっきりしたデータ変換」に一段レベルアップしていきます。
