| name | avoid-for |
| description | Avoid for loops (C-style, for...of, for...in) in TypeScript/JavaScript. Prefer higher-order Array methods like map, filter, find, some, every, reduce. Use when writing or reviewing loops or iteration over arrays, objects, Map, Set, or String. |
| user-invocable | false |
for文をなるべく避ける
for文は強力すぎて、
コードの意図が読み取りにくくなります。
構造化プログラミングにおいてgoto相当の機能を避けるのと同じ理由で、
for文もなるべく避けてください。
避ける対象は以下の全てです。
- C言語方式の
for (let i = 0; i < n; i++)
for...of
for...in
forではありませんが、
同じくループ構文の以下も避けてください。
なぜ避けるのか
for文は何でもできてしまいます。
どの操作をしているのかはループ本体を注意深く読まないと分かりません。
mapやfilterのような高抽象度の関数は、
名前を見ただけで「変換している」「絞り込んでいる」と意図が伝わります。
gotoが制御フローを自由にしすぎて構造化プログラミングで避けられたのと同じく、
forはデータの流れを自由にしすぎます。
より弱く意図の限定された関数に置き換えることで、
読み手の負担が減ります。
Arrayの場合
Arrayのメソッドでやりたいことに対応するものを選びます。
| やりたいこと | forの代替 |
|---|
| 各要素を変換する | map |
| 変換して一段平坦化する | flatMap |
| 条件で絞り込む | filter |
| 最初の一致を探す | find / findIndex |
| 最後の一致を探す | findLast / findLastIndex |
| 含まれるか調べる | includes |
| 位置を調べる | indexOf / lastIndexOf |
| 一つでも満たすか調べる | some |
| すべて満たすか調べる | every |
| 単一の値に集計する | reduce / reduceRight |
| 副作用だけ実行する | forEach |
| ネストした配列を平坦化する | flat |
| 文字列に連結する | join |
| 並べ替える | toSorted |
| 逆順にする | toReversed |
変換と絞り込みはチェーンで素直に書けます。
const result = [];
for (let i = 0; i < users.length; i++) {
if (users[i].active) {
result.push(users[i].name);
}
}
const result = users.filter((user) => user.active).map((user) => user.name);
大域脱出が必要な場合
途中で打ち切りたい(break相当の)場合は、
専用のメソッドで表現できます。
- 一つでも条件を満たすか:
some
- すべて満たすか:
every
- 最初の一致を取得:
find
let found = false;
for (const user of users) {
if (user.id === targetId) {
found = true;
break;
}
}
const found = users.some((user) => user.id === targetId);
someとeveryとfindは、
返値が確定した時点で反復を打ち切るので、
性能面でもbreakと同等です。
reduceは控えめに使用
reduceはforよりは弱いものの、
それでも強力で読みにくくなりがちです。
mapやfilterやsomeで表現できる処理をreduceで書くのは避けてください。
単純な合計のような処理はreduceが適していますが、
複雑な蓄積処理をreduceに詰め込むと意図が埋もれます。
Objectの場合
for...inではなく、
Object.keys
Object.values
Object.entries
などで配列に変換してから、
Arrayのメソッドを使います。
for (const key in obj) {
console.log(key, obj[key]);
}
Object.entries(obj).forEach(([key, value]) => {
console.log(key, value);
});
変換して新しいオブジェクトを作る場合は、
Object.fromEntriesと組み合わせます。
const upper = Object.fromEntries(
Object.entries(obj).map(([key, value]) => [key, value.toUpperCase()]),
);
MapとSetの場合
MapとSetの
などはイテレータを返します。
イテレータにはmapやfilterなどのイテレータヘルパーが生えているので、
配列に変換せずそのままチェーンできます。
最終的に配列が必要な場合だけtoArrayで配列にします。
const names = userMap
.values()
.map((user) => user.name)
.toArray();
const activeEntries = userMap
.entries()
.filter(([key, value]) => value.active)
.toArray();
const doubled = numberSet
.values()
.map((n) => n * 2)
.toArray();
MapとSetにはforEachもあるので、
副作用だけ実行したい場合は直接呼べます。
イテレータヘルパーがサポートされていない環境で実行する場合や、
イテレータが対応していないメソッドを使いたい場合は、
スプレッド構文やArray.fromで配列に変換してからArrayのメソッドを使ってください。
const names = [...userMap.values()].map((user) => user.name);
Stringの場合
文字列を1文字ずつforで回す代わりに、配列やイテレータの操作に置き換えます。
- 文字ごとに処理する: スプレッド構文
[...str]やArray.fromで配列化する
- 区切りで分割する:
split
- すべての一致を取り出す:
matchAll(イテレータを返す)
- 置換する:
replaceAll(コールバックも渡せる)
let count = 0;
for (let i = 0; i < str.length; i++) {
if (str[i] === "a") count++;
}
const count = [...str].filter((char) => char === "a").length;
サロゲートペアを含む文字列では、
[...str]はコードポイント単位で分割するため、
str[i]の添字アクセスより安全です。
例外
高抽象度の関数で素直に書けない場合
高抽象度の関数で素直に書けない場合に限り、
for...ofを使ってよいです。
awaitを直列で実行したい場合(for await...of)
Promiseの操作で問題ない場合はPromise.allなどを使ってください
- 複数の配列を同時に進めるなど複雑な制御が必要な場合
- 早期
returnで関数全体を抜けたい場合
C方式のforやfor...inよりはfor...ofの方がまだ読みやすいので、
どうしてもループが必要ならfor...ofを選んでください。
for...inはプロトタイプチェーン上のプロパティも列挙してしまうため、
ほぼ常に避けるべきです。
性能が格段に違う場合
性能が極めて重要で、
メソッドチェーンによる中間配列の生成がボトルネックになる場合は、
for ... ofを使ってよいです。
ただし実際に計測して問題だと確認できた場合に限ります。
多くの場合は可読性を優先してください。
既存コードが使っている場合
既存コードに大量のforがある場合、
それらを無理に一度に書き換える必要はありません。
新しく書くコードで高抽象度の関数を優先し、
既存のforは変更のついでに徐々に置き換えていくのが現実的です。