TypeScriptは、JavaScriptの柔軟性を維持しつつ、静的型付けによってプログラムの安全性を大幅に高めることができる言語です。

プログラム開発において繰り返し処理は欠かせない要素ですが、その中でも「条件が満たされている間だけ処理を繰り返す」while文は非常に強力です。

しかし、while文は条件分岐が動的であるため、適切に型を扱わないと実行時エラーの原因になる可能性を秘めています。

この記事では、TypeScriptにおけるwhile文の基礎から、型ガードを活用した高度で安全な反復処理の実装方法まで詳しく解説します。

TypeScriptにおけるwhile文の基本構造

TypeScriptでのwhile文の書き方は、基本的にJavaScriptと同じですが、条件式の評価結果が厳密に「boolean型」であることが強く意識されます。

まずは、最もシンプルなwhile文の構文を確認してみましょう。

TypeScript
// カウンタ変数の初期化
let count: number = 0;

// 条件式がtrueの間、ブロック内の処理が実行される
while (count < 3) {
    console.log(`現在のカウントは ${count} です。`);
    count++; // インクリメントを忘れると無限ループになるため注意
}
実行結果
現在のカウントは 0 です。
現在のカウントは 1 です。
現在のカウントは 2 です。

上記の例では、変数countが3未満である限り、処理が繰り返されます。

TypeScriptでは、変数の型がnumberとして定義されているため、比較演算の結果が論理値となることが保証されています。

do…while文による最低1回の実行保証

while文と似た構造を持つものに、do...while文があります。

while文は「実行前に条件を判定する」のに対し、do...while文は「処理を実行した後に条件を判定する」という特徴があります。

TypeScript
let status: number = 0;

do {
    console.log("少なくとも1回は実行されます。");
    status++;
} while (status < 0); // 初回から条件はfalseだが、一度は実行される
実行結果
少なくとも1回は実行されます。

このように、初期条件に関わらず最低限一度は実行させたい初期化処理などにおいて、do...while文は有用です。

ループ内での型ガードと型の絞り込み

TypeScriptでwhile文を使用する最大のメリットは、ループの条件式自体が「型ガード」として機能する点にあります。

これにより、ループ内部では変数の型が特定の状態に絞り込まれ、安全にプロパティやメソッドにアクセスできるようになります。

Nullチェックを兼ねた反復処理

例えば、配列から要素を一つずつ取り出し、要素が存在する間だけ処理を続けるケースを考えてみましょう。

TypeScript
function processQueue(queue: (string | null)[]) {
    while (queue.length > 0) {
        // shift()は要素がない場合にundefinedを返すが、lengthチェックにより存在が示唆される
        const item = queue.shift();

        // ここでitemは string | null | undefined の可能性がある
        if (item !== null && item !== undefined) {
            // このブロック内では item は string 型に絞り込まれる
            console.log(`処理中: ${item.toUpperCase()}`);
        }
    }
}

const myQueue = ["task1", null, "task2"];
processQueue(myQueue);

上記のコードでは、if文によるチェックを行うことで、nullundefinedの可能性を排除しています。

TypeScriptのコンパイラは、この条件分岐を理解し、item.toUpperCase()を呼び出す際にエラーを出しません。

型ガード関数を利用したwhile文

より複雑なオブジェクトを扱う場合、ユーザー定義の型ガード(Type Predicates)をwhileの条件式に組み込むことが可能です。

型ガードを条件式に直接書くことで、ループ内の安全性を劇的に向上させることができます。

TypeScript
interface ActiveTask {
    id: number;
    status: "active";
}

interface PendingTask {
    id: number;
    status: "pending";
}

type Task = ActiveTask | PendingTask | null;

// 型ガード関数の定義
function isActiveTask(task: Task): task is ActiveTask {
    return task !== null && task.status === "active";
}

function executeTasks(tasks: Task[]) {
    let currentTask = tasks.shift();

    // currentTaskがActiveTaskである間だけループを回す
    while (isActiveTask(currentTask)) {
        // ループ内では currentTask は ActiveTask 型として扱える
        console.log(`タスクID ${currentTask.id} を実行しています。`);
        
        currentTask = tasks.shift();
    }
}

この手法を使うと、「特定の条件を満たす連続したデータ」のみを抽出して処理するロジックが非常にスマートに記述できます。

while文と配列操作の組み合わせにおける注意点

TypeScriptで配列をwhile文で操作する際、特にpop()shift()を使用する場合は注意が必要です。

配列が空になったときにこれらのメソッドはundefinedを返しますが、TypeScriptの構成によっては、型定義上そのことが見落とされる場合があります。

配列の要素取得と型の安全性

以下の表は、while文でよく使われる配列操作と、その際の型推論の挙動をまとめたものです。

メソッド動作TypeScript上の戻り値の型
shift()配列の先頭から要素を取り除くT | undefined
pop()配列の末尾から要素を取り除くT | undefined
Array[index]指定した添字の要素を参照するT (noUncheckedIndexedAccessが無効な場合)

noUncheckedIndexedAccessフラグが有効なプロジェクトでは、インデックスによるアクセスもundefinedを含む型として扱われます。

そのため、while文の条件式でしっかりと実体の存在をチェックすることが、ランタイムエラーを防ぐ鍵となります。

安全な反復処理のためのベストプラクティス

while文を安全に活用するためには、単に構文を知っているだけでなく、いくつかの設計上の原則を守ることが重要です。

ここでは、TypeScriptの型システムを活かした保守性の高いループの書き方を紹介します。

無限ループの防止と不変条件の維持

while文における最大の不具合は、終了条件に到達しない無限ループです。

TypeScriptでは、ループ内で条件に関わる変数が正しく更新されているかを型レベルで完全に監視することは困難です。

そのため、ループ変数の更新にはreadonlyな型を避ける、あるいは再代入可能なローカル変数として明示的に扱う必要があります。

TypeScript
function searchSomething(items: string[], target: string) {
    let index = 0;
    let found = false;

    while (index < items.length && !found) {
        if (items[index] === target) {
            found = true;
            // 目的のものが見つかったら即座にループを抜ける構成にする
        } else {
            index++;
        }
    }
    return found;
}

このように、複数の条件(インデックスの範囲内であること、かつ未発見であること)を組み合わせる際は、論理演算子を適切に使用してください。

「break」と「continue」の適切な利用

ループの制御を柔軟にするためにbreak(中断)とcontinue(スキップ)が使われます。

これらを使用する際も、どのタイミングで変数の型が変わるのかを意識することが大切です。

TypeScript
let i = 0;
while (i < 10) {
    i++;
    if (i % 2 === 0) {
        continue; // 偶数の場合はこれ以降の処理をスキップ
    }
    console.log(`奇数: ${i}`);
}

continueが呼ばれると、それ以降のコードパスは実行されないため、TypeScriptはその後の処理における変数の状態を正しく解析できます。

非同期処理とwhile文の組み合わせ(Async/Await)

現代のアプリケーション開発では、APIのポーリング(定期的な状態確認)などで非同期のwhile文が必要になる場面が多くあります。

TypeScriptなら、非同期処理においても型安全を維持しながらループを記述できます。

TypeScript
async function waitForStatusUpdate(id: string) {
    let isComplete = false;

    while (!isComplete) {
        const response = await fetch(`/api/status/${id}`);
        const data: { status: string } = await response.json();

        if (data.status === "completed") {
            isComplete = true;
            console.log("完了しました。");
        } else {
            // 次の確認まで1秒待機する
            await new Promise(resolve => setTimeout(resolve, 1000));
        }
    }
}

このパターンでは、awaitを使用して非同期処理の完了を待機しているため、メインスレッドをブロックすることなく安全にループを継続できます。

また、data.statusの型定義を適切に行うことで、マジックストリングによるミスを防ぐことも可能です。

while文とfor文の使い分け

多くのエンジニアが「いつwhile文を使い、いつfor文を使うべきか」という疑問を持ちます。

結論から言えば、「繰り返す回数が事前に決まっているか」が判断の基準になります。

  • for文: 配列の全要素走査や、回数指定のループに適している。
  • while文: 「特定の状態に変化するまで」といった、動的な条件に基づいたループに適している。

TypeScriptにおいては、イテレータやジェネレータを扱う際にもwhile文が頻繁に登場します。

例えば、next()メソッドを持つオブジェクトを反復処理する場合、doneプロパティがtrueになるまで繰り返す構造はwhile文が最も適しています。

よくあるエラーと解決策

TypeScriptでwhile文を書く際につまずきやすいポイントとその対策を整理します。

1. 条件式内での代入による型推論ミス

JavaScriptではwhile (node = node.next)のような書き方が許容されますが、TypeScriptでは設定によってエラーとなる場合があります。

これは代入と比較の打ち間違いを防ぐための制約ですが、意図的に使う場合はカッコで囲むなどの工夫が必要です。

2. 型の絞り込みがループ内で維持されない

ループ内で外部の関数を呼び出している場合、TypeScriptは「外部関数によって変数の状態が変わったかもしれない」と判断し、型の絞り込みをリセットすることがあります。

この場合は、ループ内で再度ローカル変数にコピーするなどの対策が有効です。

実戦例:連結リストのトラバーサル

while文の真価が発揮される例として、連結リスト(Linked List)の操作を挙げてみましょう。

ノードが次々とリンクされている構造を辿る処理は、while文が得意とする領域です。

TypeScript
interface ListNode<T> {
    value: T;
    next: ListNode<T> | null;
}

function printList<T>(head: ListNode<T> | null) {
    let current = head;

    // current が null になるまで繰り返す
    while (current !== null) {
        // ここでは current が ListNode<T> 型であることが保証される
        console.log(current.value);
        current = current.next;
    }
}

このシンプルなコードの中で、TypeScriptはcurrent.valueへのアクセスが安全であることを常に監視しています。

もしcurrent = current.nextの代入を忘れると、型推論以前にロジックとしての欠陥(無限ループ)になりますが、currentnullを許容する型であるため、最終的にループが終了する構造を型の上でも表現できています。

まとめ

TypeScriptにおけるwhile文は、単なる繰り返しのための構文ではなく、型システムと密接に連携した強力な制御フローです。

条件式による型の絞り込みを正しく理解し、型ガードを積極的に活用することで、JavaScript以上に堅牢で読みやすいループ処理を実現できます。

特に、動的なデータ構造の操作や非同期のポーリング処理においては、while文が持つ柔軟性が大きな武器になります。

今回解説した型安全な反復処理のポイントを意識して、日々のコーディングに役立ててください。

無限ループには常に注意を払い、終了条件が論理的に正しいかを型定義と共に確認する習慣をつけましょう。

適切な制御フローの選択と厳密な型管理が、大規模なプロジェクトにおいてもバグの少ないクリーンなコードを生み出す近道となります。