JavaScript/TypeScriptにおいて、配列を元に配列やオブジェクト(連想配列)を作るときArray.prototype.reduce()
が便利です。
しかし、reduce()
に引数として渡す縮小関数(コールバック関数)が非同期関数(async)になると少し複雑です。
そこでこの記事では、Array.prototype.reduce()
のコールバック関数として非同期関数を渡すときの注意点を説明します。
例えば、ユーザーIDの配列を受け取ってデータベースにアクセスし、ユーザーデータを取得して特定のユーザーのみの配列を返す処理を reduce()
を使って次のように書いたとします。
const userIdToUser = await userIds.reduce(async (acc, userId) => {
const user = await fetchUserByUserId(userId);
if (user.isActive) {
acc = acc.concat(user);
}
return acc;
}, []);
最初に reduce()
関数について簡単に復習しておきましょう。
reduce()
関数の第一引数には縮小関数となるコールバック関数を渡します。このコールバック関数の戻り値は、配列の次の要素と共に次のコールバック関数の引数となります。
余談となりますが、reduce()
のコールバック関数の第1引数には acc という名前がよく使われます。
これは accumulator から来ており、累積を意味しています。
話を戻して、上記のスクリプトは下記のようなエラーになってしまいます。
Uncaught TypeError: acc.concat is not a function
エラーの内容は、acc.concat
が関数ではないというものです。いったいどういうことでしょうか?
さらに詳しく見るため console.log で acc がどのように変化しているか見てみましょう。
const userIdToUser = await userIds.reduce(async (acc, userId) => {
+ console.log(acc);
const user = await fetchUserByUserId(userId);
if (user.isActive) {
acc = acc.concat(user);
}
+ console.log(acc);
return acc;
}, []);
結果は下記のようになりました。
[]
Promise {<pending>}
Promise {<pending>}
1巡目の最初の acc
は初期値の空配列になっていますが、以降は Promise オブジェクトになっています。
先ほど説明した通り、reduce()
は前回のコールバック関数の戻り値が次のコールバック関数の第一引数 acc
に渡ります。今回のケースではコールバック関数は非同期関数(async)になっています。
async 付きの非同期関数は戻り値は Promise オブジェクトになります。
すなわち、2巡目以降のループでは acc
がPromise オブジェクトになり、Promise オブジェクトに対して concat()
を呼び出そうとしてエラーになっているというわけでした。
前のコールバック関数が Promise オブジェクトになってしまうのが原因なので、Promise を解決して値を取得すれば良いということになります。
下記のようにコールバック関数の冒頭で acc
の Promise を解決します。
const userIdToUser = await userIds.reduce(async (acc, userId) => {
+ acc = await acc;
const user = await fetchUserByUserId(userId);
if (user.isActive) {
acc = acc.concat(user);
}
return acc;
}, []);
期待通りに値を得ることができました。
[
{id: 'hoge', isActive: true},
{id: 'fuga', isActive: true},
{id: 'piyo', isActive: true}
]
以上です。
この記事では、Array.prototype.reduce()
のコールバック関数として非同期関数を渡すときの注意点を説明しました。
コメントを送る
コメントはブログオーナーのみ閲覧できます