皆さんはArray.prototype.reduce
を使ったことがあるでしょうか。
よくredcue
はその使い方の例として下記のように配列に格納された数値を全部足した結果を返却する処理が示されます。
// 与えられた配列の値をすべて足した値を返却する例
arr.reduce((acc, current) => acc + current, 0);
しかし、reduce
というメソッドは非常に柔軟で汎用的な配列操作が行えるメソッドでもあります。
reduceを利用して他のメソッドを実装してみる
reduce
の汎用性を確かめるために、他の配列処理をreduce
を使って再現してみようと思います。
文章量の限界もあるので全てのメソッドを記述することは出来ないため一部だけの紹介ですが、大抵の処理はreduce
を利用することで記述可能です。
map
Array.prototype.map
は対象となる配列全てに関数を適用し、新しい配列を返す関数です。
この例ではnumber[]
である配列arr
の各要素を+1
した配列を新しく生成します。
// mapでの処理
arr.map((v) => v + 1);
// reduceを使った処理
arr.reduce((acc, current) => {
const newValue = current + 1;
return [...acc, newValue];
}, []);
find
Array.prototype.find
はコールバック関数を評価して、最初にtrue
を返した値自身を返す関数です。
この例ではnumber[]
の中から10以上である値を返す処理を記述します。
// findでの処理
arr.find((v) => v > 10);
// reduceを使った処理
arr.reduce((acc, current) => {
if(acc !== undefined) {
return acc;
};
return current > 10 ? current : undefined;
}, undefined);
toReversed
Array.prototype.toReversed
は受け取った配列の順番を反転させた配列を返すメソッドです。
この例では、unknown[]
である配列arr
を反転させた配列を返す処理を記述します。
// toReversedでの処理
arr.toReversed();
// reduceを使った処理
arr.reduce((acc, current) => [current, ...acc], []);
reduceの汎用性
基本的には専用のメソッドを使ったほうが明示的かつ簡潔に処理を記述できるため、実務でこのような書き換えを行うことは殆どないかと思います。
しかしながら、reduce
というメソッドの汎用性はなんとなく分かる内容になったかと思います。
いつ使うのか
基本的には専用のメソッドを利用したほうがいいと思いますが、複数回処理をチェーンして続ける場合にreduce
の方が簡潔に記述できる場合があります。
例として記事データが格納されているPosts
型を下記のように処理するコードを書く場合を例にそれぞれのコードの書き方を比較してみましょう。
-
currentUserId
とuserId
が一致するものだけを抽出 -
publishDate
とlastModified
はDate型に変換 -
publishDate
を基準に並べ替えを行う
// 共通の型定義
type Posts = {
userId: string,
title: string,
publishDate: string,
lastModified?: string,
}[];
// reduceを使わない例
const currentUserId = 'foo';
const result = posts // Post[]型
// `currentUserId`と`userId`が一致するものだけを抽出
.filter((post) => post.userId === currentUserId)
// `publishDate`と`lastModified`はDate型に変換
.map((post) => {
return {
...post,
publishDate: new Date(post.publishDate),
lastModified: post.lastModified ? new Date(post.lastModified) : undefind,
};
})
// `publishDate`を基準に並べ替えを行う
.toSorted((prev, next) => {
return prev.publishDate.getTime() - next.publishDate.getTime();
});
// reduceを使う例
const currentUserId = 'foo';
const result = posts // Post[]型
.reduce((acc, current) => {
// 必要な値の準備
const publishDate = new Date(current.publishDate);
const lastModified = current?.lastModified ? new Date(current.lastModified) : undefined;
const isOld = (acc?.publishDate.getTime() ?? 0) > current.publishDate.getTime();
// 返却する値の(構造)定義
if (current.userId === currentUserId) {
return acc;
};
return isOld ?
[...acc, {...current, publishDate, lastModified}]:
[{...current, publishDate, lastModified}, ...acc];
}, []);
上記のコードは動かないんですが、なんとなくやりたいこととそれぞれのコードの書き方で表現した場合にどのようなコードになるのかという目安だと思ってください。
reduce
を使わない例はそれぞれの処理が明確に別れていて、データを抽出して型を変換して、ソートするという流れが追いやすいコードになっています。
reduce
を使ったコードの場合、はじめに必要な値を作成してその値をどのような構造で返すかという記述になっています。
個人的には前者のコードは仕様の変更が入った場合に、それぞれの処理の中で値がどのように変化しているかを意識する必要があるのに対して、後者に関しては一つの関数スコープの中で記述が収まっているため仕様の変更にも対応しやすいコードになっていると感じます。
まとめ
普段あまりreduce
を使わない人も、汎用性の高いメソッドであることを理解して使えるようになっておくとよりよいコードが書けるようになるかもしれません。