⚠️ reduce() のつらいところ
acc の中身を追いながら読まないと処理が頭に入ってこない書くたびに「このイディオム、何度目だ」と思っていました。
実はこのコード、最初は if (!acc[task.priority]) { acc[task.priority] = [] } のあとに acc[task.priority].push(task) と書いていたのですが、TypeScriptに Object is possibly 'undefined' と怒られました。if 文で存在チェックをしていても、TypeScriptはインデックスアクセス(acc[task.priority])の再参照までは絞り込んでくれないためです。group という変数にいったん受けてから push する形に直して、ようやくコンパイルが通りました。
同じ処理を Object.groupBy() で書き直すとこうなります。
const grouped = Object.groupBy(tasks, (task) => task.priority);1行です。第一引数にグループ化したい配列(イテラブル)、第二引数にキーを返すコールバック関数を渡すだけで、キーごとに配列へ振り分けたオブジェクトが返ってきます。
キーの存在チェックや配列の初期化を自前で書く必要がなく、「priorityでグループ化している」という意図がそのままコードの形になっているのが気持ちいいところです。
🛠 返り値は Partial<Record<K, T[]>> 型になる
TypeScriptの型定義上、Object.groupBy() の返り値は Record<K, T[]> ではなく Partial<Record<K, T[]>> になります。
const grouped = Object.groupBy(tasks, (task) => task.priority);// grouped: Partial<Record<"high" | "mid" | "low", Task[]>>console.log(grouped.high); // Task[] | undefined存在しないキーにアクセスすると undefined になる可能性を型が正しく表現しているためです。reduce() の実装で先ほど Object is possibly 'undefined' に遭遇したのも、根っこは同じ話でした。acc の型注釈を Record<string, Task[]> と書いた時点で「どのキーにも必ず配列がある」ことになっているので、TypeScript的には存在チェックなど不要なはずなのに、実際にはチェックしないと安全に書けない。型注釈と実態がズレていたわけです。
改めて見比べると、型面での優位性は2つあると思っています。
reduce() では Record<string, Task[]> のようにキーを string へ広げて書きがちですが、Object.groupBy() はコールバックの返り値から "high" | "mid" | "low" というリテラル型をそのまま推論してくれますundefined の可能性を型で強制する:reduce() で Record<string, Task[]> と注釈しても、実際には存在しないキーが undefined になり得ます。この「型注釈と実態のズレ」に気づかせてくれるのが noUncheckedIndexedAccess というコンパイラオプションですが、有効にしていないプロジェクトも多いはずです。Object.groupBy() はこの可能性を Partial として返り値の型に最初から織り込んでいるので、オプションの有無に関わらず正直な型になります👉 ポイント
グループ化した結果を使う側では、grouped.high?.length のようにオプショナルチェイニングを添えるのが安全です。
キーは文字列に限らず、コールバックの返り値次第で数値や真偽値でも使えます。
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];const byEvenOdd = Object.groupBy(numbers, (n) => (n % 2 === 0 ? "even" : "odd"));// { even: [2, 4, 6, 8, 10], odd: [1, 3, 5, 7, 9] }コールバックの第二引数にインデックスも渡ってくるので、インデックスを使ったグループ化(例:3件ごとにまとめる)にも対応できます。
const chunked = Object.groupBy(numbers, (_, index) => Math.floor(index / 3));// { 0: [1, 2, 3], 1: [4, 5, 6], 2: [7, 8, 9], 3: [10] }キーに文字列・数値・真偽値以外(オブジェクトなど)を使いたい場合は、同時に追加された Map.groupBy() を使います。
const usersMap = Map.groupBy(users, (user) => user.department);こちらは Object ではなく Map を返すので、キーの型に制約がありません。用途に応じて使い分けます。
reduce() で書くグループ化処理は「動くけど毎回同じことを書いている」感覚が強く、地味にストレスでした。
Object.groupBy() に置き換えてみると、コード量が減るだけでなく「グループ化している」という意図がそのまま読み取れるようになったのが一番の収穫です。
Node.js 21以降・主要モダンブラウザでサポートされているので、実行環境を選べるプロジェクトでは積極的に使っていきたいと思います。