【JavaScript/TypeScript】forEachは非同期!Promiseの使い方に要注意 ~JS、TSにおけるfor文の比較~

はじめまして、エンジニアの杉崎です。
今回は私がTypeScriptを使ったプロジェクトに参画してすぐの頃、forEach文を使おうとした際に非同期処理の待機について罠にはまったので、for系メソッドの比較と注意点について書いてみました。

【種類】

インデックスを用いたfor文、配列の全要素をループするforEach文とfor-of文がある。

1. 普通のfor文

const numbers: number[] = [1, 2, 3, 4, 5];
for (let i = 0; i < numbers.length; i++) {
  console.log(numbers[i]);
}
  • パフォーマンスは一番軽い
  • インデックス(今何個目か)を使用して何かしたい場合に便利

2. forEach文

const numbers: number[] = [1, 2, 3, 4, 5];
numbers.forEach((number) => {
  console.log(number);
});
  • 個数を敢えて書かずとも全要素回せて便利
  • 何の配列を回しているか見えやすい
  • パフォーマンスは他の2つに比べてやや劣る場合がある

3. for-of文

const numbers: number[] = [1, 2, 3, 4, 5];
for (const number of numbers) {
  console.log(number);
}
  • forEach文よりやや軽い
  • 配列が後ろに来るためforEach文よりやや読みづらい?

【注意】

ループ内で非同期処理を行う場合、forEach文では完了を待てない

・だめな例

const numbers: number[] = [1, 2, 3, 4, 5];
numbers.forEach((number) => {
  await asyncFunction(number);
});
log('この時点ではasyncFunctionが全て終わっていない場合がある')

・いける例

1. for文、for-of文
const numbers: number[] = [1, 2, 3, 4, 5];
for (const number of numbers) {
  await asyncFunction(number);
});
log('asyncFunctionが全て終わってからここにくる')
2. Promise.allを使う
const numbers: number[] = [1, 2, 3, 4, 5];
const promises = numbers.map(async (number ) => {
  await asyncFunction(number );
});
await Promise.all(promises);
log('asyncFunctionが全て終わってからここにくる')

なおfor-ofではasyncFunctionは順次に実行、promise.allの方法では並行して非同期で実行される

まとめ

インデックスを使いたいならfor、要素だけでいいならfor-of、非同期処理を中で行うならfor-ofもしくはforEachでPromise.allで待機する、といった感じでしょうか。
もっとも、for-ofとforEachのパフォーマンスについては多くの場合そこまで意識する程ではないと思うので、他言語の経験者も読みやすいforEachでもいいのかもしれません。
ただしその場合、実行順は必ずしも保証されないので注意して確認した方が良いと思われます。

以上、私がTypeScriptのforループで躓いた点でした。

参考文献

https://isub.co.jp/typescript/loop-async-await-promise/
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/for…of
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach

関連記事

  1. Unityでメモリ位置を表示するツール

  2. Android15(APIレベル35)への対応が本当にできたのか不安な…

  3. GitHub Actionsで1つしかキューされない挙動について

サービスサイト