TypeScript

TypeScriptのユニオン型と型ガード:型の絞り込みからneverによる網羅チェックまで

この記事でわかること

string | numberの分岐から、状態を安全に表す判別可能なユニオン型まで解説。typeof、unknown、neverの役割を、配送状態を扱うコード例と練習問題で学びます。

関数の引数に「文字列または数値」を渡したいとき、すべてを any にすると、存在しないメソッドの呼び出しを見逃してしまいます。TypeScriptでは、取り得る型をユニオン型で列挙し、条件分岐で扱える型を狭める設計ができます。この記事では、関数と基本的な型の書き方が分かる方向けに、入力値の整形と配送状態の表示を実装します。

【この記事でできるようになること】

・string | number を安全に処理する。

・「処理中・成功・失敗」と、それぞれに必要なデータを一緒に表す。

・状態を追加したときに、処理の書き忘れをコンパイラで見つける。

【1. ユニオン型は、取り得る型の候補を表す】

string | number は、値が文字列か数値のどちらかであることを表します。文字列にだけある trim() を無条件で呼ぶことはできません。まず typeof で数値を判定し、数値ならその場で結果を返します。残る経路では文字列として扱えます。

ファイル: reference.ts

function formatReference(value: string | number): string {
if (typeof value === "number") {
return `No.${value.toFixed(0)}`;
}
return value.trim().toUpperCase();
}

console.log(formatReference(42)); // No.42
console.log(formatReference(" ab-7 ")); // AB-7

このように、条件や処理の流れを使って型の候補を減らすことを「型の絞り込み(narrowing)」と呼びます。typeof など、判定を通じて型を絞り込む仕組みが型ガードです。詳しい判定規則は 公式のNarrowing(https://www.typescriptlang.org/docs/handbook/2/narrowing.html) で確認できます。

【2. 成功したときだけ宛先を持つ状態を表す】

配送状態を単なる文字列と、任意の宛先・エラー文の組み合わせで表すと、「成功なのに宛先がない」データまで作れてしまいます。そこで状態ごとにオブジェクトの形を分けます。共通の status に異なる文字列リテラルを割り当てると、その値からオブジェクト全体の型を判別できます。これが判別可能なユニオン型です。

ファイル: delivery.ts

type DeliveryState =
| { status: "pending" }
| { status: "sent"; recipient: string }
| { status: "failed"; reason: string };

function assertNever(value: never): never {
throw new Error(`想定外の状態: ${JSON.stringify(value)}`);
}

function describeDelivery(state: DeliveryState): string {
switch (state.status) {
case "pending":
return "配送待ち";
case "sent":
return `配送完了: ${state.recipient}`;
case "failed":
return `配送失敗: ${state.reason}`;
default:
return assertNever(state);
}
}

console.log(describeDelivery({ status: "pending" }));
console.log(describeDelivery({ status: "sent", recipient: "reader@example.com" }));
console.log(describeDelivery({ status: "failed", reason: "宛先不明" }));

出力は順に「配送待ち」「配送完了: reader@example.com」「配送失敗: 宛先不明」です。sent の分岐では recipient を、failed の分岐では reason を使えます。別の分岐で違うプロパティに触れると型エラーになり、状態とデータの取り違えを防げます。

【3. neverで、分岐の追加漏れに気付く】

すべての状態を処理した後、default に残る候補はありません。その値の型が never です。例えば型に { status: "retrying"; attempt: number } を追加し、switchを変更しないで型チェックすると、assertNever(state) に渡せる型ではなくなりエラーになります。追加した状態の処理を書けば解消します。

この設計は、画面の読み込み状態、注文の状態、ファイル変換の結果などにも使えます。例外を投げる部分は想定外の実行時データへの最後の備えですが、型チェックそのものが外部データを検証してくれるわけではありません。

【4. よくある落とし穴】

・if (value) は空文字や0も除外します。「値がない場合」だけを除きたいなら、nullやundefinedを明示的に判定します。

・typeof null は "object" です。オブジェクトとして扱う前にnullも除外してください。

・as DeliveryState と書いても、JSONの中身は検証されません。外部入力は unknown として受け、必要なフィールドを実行時に確認する設計にします。

【実行方法と練習問題】

TypeScriptがインストールされた作業用フォルダで、上の例をそれぞれ reference.ts、delivery.ts に保存します。次の設定で npx tsc -p . を実行し、node dist/reference.js と node dist/delivery.js で出力を確認できます。既存プロジェクトの設定は上書きせず、独立したフォルダで試してください。

ファイル: tsconfig.json

{"compilerOptions":{"target":"ES2022","module":"NodeNext","strict":true,"outDir":"dist","types":[],"lib":["ES2022","DOM"]},"include":["*.ts"]}

練習として retrying 状態を追加し、最初に型エラーが出ることを確認してください。その後、再試行回数を表示する分岐を追加します。default を単に「不明」と表示する処理に変えた場合との違いも考えてみましょう。

【よくある質問】

【型ガードを書けばanyは不要ですか?】

何が入るか分からない値は unknown として受け、確認してから使う方法をまず検討できます。ただし検証内容は必要な構造に応じて増えるため、複雑な外部データでは検証用ライブラリも選択肢になります。

【型を追加したら、実行中のアプリも自動で変わりますか?】

変わりません。型は開発時の検査に使われます。ソースを修正し、型チェックとビルドを行い、生成したアプリを実行・配布する必要があります。

【参考資料】

・TypeScript Handbook: Narrowing(https://www.typescriptlang.org/docs/handbook/2/narrowing.html)

・TypeScript Handbook: Everyday Types(https://www.typescriptlang.org/docs/handbook/2/everyday-types.html)

y.
WRITTEN BY

y_ymo10

SEの部屋で、TypeScript・Next.jsの開発ノートを公開しています。

ほかの開発ノートを読む →