エンジニアを目指す初学者に向けて、わかりやすく解説したブログです。

for文ではなくfilter()を使ってリストの抽出や除外をしよう

はじめに

配列から条件に合う要素だけを抽出したいとき、次のようなコードを書いていないだろうか。

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文でフィルターを実装する場合、結果を格納する配列をletconstで宣言し、条件に合う要素を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 除外するときに最適である。

前の例では、タスク一覧から完了済みタスクの抽出を行っている。

Image in a image block

それでも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()など他の配列メソッドも覚えると、さらにコードがシンプルになる。

関連記事

📄Arrow icon of a page linkfor文ではなくmap()を使って配列変換をしよう