TypeScript の switch 文で網羅性をチェックするテクニック
TypeScript で switch 文を記述する際に default で何を return するべきか悩むことはありませんか。ユニオン型を switch 文で処理する場合、すべてのケースが処理されているかを確認する「網羅性チェック」というテクニックがあります。この記事では、never
型を活用した手法を詳しく解説します!
網羅性チェックとは
網羅性チェックとは、ユニオン型の取りうるすべての値が適切に処理されていることを、TypeScript のコンパイラに確認させる技術です。新しい型が追加された際に、処理の漏れを防ぐことができます。
基本的な型定義
ユーザー権限管理システムを例に、基本的な型を定義します
interface AdminUser {
type: "admin";
userId: string;
permissions: string[];
}
interface RegularUser {
type: "user";
userId: string;
subscriptionLevel: "free" | "premium";
}
type User = AdminUser | RegularUser;
never 型を使った網羅性チェック
TypeScript の never
型は「決して到達しない」ことを表現する型です。すべてのケースが処理されている場合、default 節の値は never
型となります。
基本的な網羅性チェック
function processUser(user: User) {
switch (user.type) {
case "admin":
break;
case "user":
break;
default:
user; // user は never 型となる
}
}
なぜ user が never 型になるのか
ここで重要なのは、TypeScript の型推論の仕組みです。
-
関数開始時点:
user
はUser
型(AdminUser | RegularUser
) -
case "admin" を通過後:
user
はRegularUser
型に絞り込まれる -
case "user" を通過後:
user
は何の型でもない状態になる
TypeScript は各 case 文を通過するたびに、残りの可能性を除外していきます。すべての可能性が除外された結果、default 節に到達する時点では「どの型でもない」状態、つまり never
型になります。
これは TypeScript が「このコードは実行されることがない」と判断していることを意味します。
新しい型を追加した場合
新しい GuestUser
型を追加してみましょう。
interface GuestUser {
type: "guest";
sessionId: string;
expiresAt: Date;
}
type User = AdminUser | RegularUser | GuestUser;
この時点で、既存の processUser
関数は GuestUser
を処理していないため、default 節の user
は GuestUser
型となり、網羅性が不完全であることがわかります。
function processUser(user: User) {
switch (user.type) {
case "admin":
break;
case "user":
break;
default:
user; // user は GuestUser 型(never 型ではない!)
}
}
型推論の流れ:
-
関数開始時点:
user
はUser
型(AdminUser | RegularUser | GuestUser
) -
case "admin" を通過後:
user
はRegularUser | GuestUser
型 -
case "user" を通過後:
user
はGuestUser
型 -
default 節:
user
はGuestUser
型のまま
つまり、処理されていない型が残っているため、never
型にならずに具体的な型(GuestUser
)が残ります。これが網羅性チェックの仕組みです。
assertUnreachable 関数を使った実用的な方法
この問題を解消するアプローチとして、assertUnreachable
関数を使用します。
function assertUnreachable(value: never): never {
throw new Error(`Missed a case! ${value}`);
}
function getUserDashboard(user: User): string {
switch (user.type) {
case "admin":
return "管理者ダッシュボード";
case "user":
return user.subscriptionLevel === "premium"
? "プレミアムダッシュボード"
: "通常ダッシュボード";
default:
assertUnreachable(user);
// Error: type 'GuestUser' is not assignable to parameter of type 'never'
}
}
これによって、user は never ではないので、GuestUser の未定義に気づくことができます。
GuestUser
型の処理を追加すると、エラーが解消されます
function getUserDashboard(user: User): string {
switch (user.type) {
case "admin":
return "管理者ダッシュボード";
case "user":
return user.subscriptionLevel === "premium"
? "プレミアムダッシュボード"
: "通常ダッシュボード";
case "guest":
return "ゲストダッシュボード";
default:
assertUnreachable(user); // OK
}
}
戻り値の型アノテーションを使った網羅性チェック
関数の戻り値型を明示的に指定することで、すべてのパスで値が返されているかをチェックできます
function getUserPermissionLevel(user: User): number {
// Function lacks ending return statement and return type does not include 'undefined'.
switch (user.type) {
case "admin":
return 100;
case "user":
return user.subscriptionLevel === "premium" ? 50 : 10;
// GuestUser の処理が漏れている場合、コンパイルエラーになる
}
}
以下のように guest のケースを追加することでエラーを解消できます。
function getUserPermissionLevel(user: User): number {
switch (user.type) {
case "admin":
return 100;
case "user":
return user.subscriptionLevel === "premium" ? 50 : 10;
case "guest":
return 1;
default:
return assertUnreachable(user);
}
}
テンプレートリテラル型を使った複数値の組み合わせ
複数の値の組み合わせを網羅的にチェックしたい場合は、テンプレートリテラル型を活用できます
type NotificationStatus = "read" | "unread" | "archived";
type Priority = "high" | "medium" | "low";
function handleNotification(status: NotificationStatus, priority: Priority) {
const combination =
`${status}-${priority}` as `${NotificationStatus}-${Priority}`;
switch (combination) {
case "unread-high":
console.log("緊急通知を表示");
break;
case "unread-medium":
case "unread-low":
console.log("通常通知を表示");
break;
case "read-high":
case "read-medium":
case "read-low":
console.log("既読通知を非表示");
break;
case "archived-high":
case "archived-medium":
case "archived-low":
console.log("アーカイブ済み通知を処理");
break;
default:
assertUnreachable(combination);
}
}
まとめ
TypeScript の網羅性チェックは、型安全なコードを書く上で重要なテクニックです。主要なポイントは以下の通りです。
- never 型の活用: すべてのケースが処理されているかを確認
- assertUnreachable 関数: 実用的で分かりやすい方法
- 戻り値の型アノテーション: 関数の完全性を保証
- テンプレートリテラル型: 複数値の組み合わせを網羅的にチェック
これらの技術を適切に使用することで、リファクタリング時の見落としを防ぎ、より堅牢な TypeScript コードを書くことができます。
Discussion