はじめに
配列から条件に合う要素だけを抽出したいとき、次のようなコードを書いていないだろうか。
type Task = {
id: number;
title: string;
completed: boolean; // 完了したタスクはここがtrueになる
};
// TODOリストのタスク一覧
const tasks: Task[] = [
{ id: 1, title: "設計書を書く", completed: true },
{ id: 2, title: "実装する", completed: false },
{ id: 3, title: "レビューを依頼する", completed: false },
];
// 完了済みタスクを抽出する
const completedTasks: Task[] = [];
for (let i = 0; i < tasks.length; i++) {
if (tasks[i].completed) {
completedTasks.push(tasks[i]);
}
} このコードは動作する。しかし、filter()を使えばもっとシンプルに書ける。
const completedTasks = tasks.filter((task) => task.completed); 本記事では、なぜfor文ではなくfilter()を使うべきなのかを解説する。
for文によるフィルターの問題点
1. イミュータブルではない
for文でフィルターを実装する場合、結果を格納する配列をletやconstで宣言し、条件に合う要素をpush()で追加していく。
const completedTasks: Task[] = []; // 空配列を用意
for (let i = 0; i < tasks.length; i++) {
if (tasks[i].completed) {
completedTasks.push(tasks[i]); // 配列を変更
}
} この方式では、配列の中身が変更される。
constで宣言していても、配列自体への参照が変わらないだけで、中身は変わってしまう。
変数が可変であるため、意図しない箇所で配列が変更されるリスクがある。
サンプルコードとしてはTypeScriptを想定しているが
プログラミング言語によっては「追加と削除ができるリスト(ミュータブル)」と「追加と削除ができないリスト(イミュータブル)」を使い分けているものが存在し、
その場合は「追加と削除ができないリスト」を使うことができない。
2. 副作用が存在する前提のコード
上記のコードでは、ループの外で宣言されたcompletedTasksをループ内で変更している。これは副作用である。
副作用があるコードは、変数がどこで変更されるかを追跡する必要がある。コードが長くなると、追跡が困難になり、バグの原因となりやすい。
3. 冗長である
for文でフィルターを実装するには、以下の記述が必要になる。
- ループ変数
iの初期化 - 配列の長さの参照
- インデックスのインクリメント
- 条件分岐
-
push()の呼び出し
やりたいことは「完了済みタスクを抽出する」だけなのに、本質的でないコードが多い。
filter()によるフィルター
filter()を使えば、上記の問題をすべて解決できる。
const completedTasks = tasks.filter((task) => task.completed); 1. イミュータブルである
filter()は元の配列を変更せず、新しい配列を返す。元のtasks配列はそのまま残る。
const completedTasks = tasks.filter((task) => task.completed);
console.log(tasks.length); // 3(元の配列は変更されていない)
console.log(completedTasks.length); // 1 さらに、TypeScriptの場合は戻り値をreadonly型で受け取れば、その後の変更も型レベルで防ぐことができる。
const completedTasks: readonly Task[] = tasks.filter((task) => task.completed);
completedTasks.push({ id: 4, title: "追加タスク", completed: true });
// コンパイルエラー: Property 'push' does not exist on type 'readonly Task[]' for文ではpush()を使う前提のためreadonlyを適用できないが、filter()なら完全にイミュータブルなコードを実現できる。
2. 宣言的で簡潔である
filter()という名前自体が「フィルターする」という意図を表している。コードを読んだ人は、for文の中身を追わなくても、何をしているかがすぐにわかる。
// for文は何をしているか読み解く必要がある
const completedTasks: Task[] = [];
for (let i = 0; i < tasks.length; i++) {
if (tasks[i].completed) {
completedTasks.push(tasks[i]);
}
}
// filter()は「フィルターする」と宣言している
const completedTasks = tasks.filter((task) => task.completed); filter()の考え方
filter()は、あるリストデータから要素を抽出 or 除外するときに最適である。
前の例では、タスク一覧から完了済みタスクの抽出を行っている。
それでもfor文を使いたくなった場合
continueでスキップしたい場合
for文でcontinueを使って特定の要素をスキップしたい場合がある。
const results: Task[] = [];
for (let i = 0; i < tasks.length; i++) {
if (!tasks[i].completed) {
continue; // 未完了タスクはスキップ
}
results.push(tasks[i]);
} これはfilter()の条件でfalseを返すことと同じである。
const results = tasks.filter((task) => task.completed); continueの実装は、確実に filter()で書くことができる。
breakで途中終了したい場合
for文でbreakを使って途中でループを抜けたい場合は、filter()ではなくfind()が適している。
// 最初に見つかった完了済みタスクを取得
const firstCompleted = tasks.find((task) => task.completed); breakを使いたくなったら、find()やsome()、every()など、別のメソッドを検討しよう。
これについては別の記事で詳しく解説する。
まとめ
配列のフィルター処理において、for文が本当に必要なケースはほぼ存在しない。
filter()を使うことで、より安全で読みやすく、保守しやすいコードを書くことができる。
for文を書きそうになったら、filter()を思い出そう。
そして、map()やfind()など他の配列メソッドも覚えると、さらにコードがシンプルになる。