
never type
never type
exhaustive check
exhaustive check
type guard
type guard
discriminated union
discriminated union
TypeScriptはコンパイル時に多くのバグを捕捉します。しかし、TypeScriptを単なる便利なアシスタントから容赦ない監査役に変えるパターンが1つあります。それがnever型を使った網羅的チェックです。その違いは、switchでケースが欠落していても、「これは未定義かもしれない」という曖昧な警告ではなく、忘れた正確な行でハードエラーが発生することです。
進化するコードベースでは、enumのようなswitchステートメントはすべて時限爆弾です。3つのケースに対するハンドラを記述します。3ヶ月後、誰かが4番目のバリアントを追加します。コンパイルは通ります。テストもパスします。本番環境では新しいケースが黙って無視されます。顧客が報告するまでエラーは発生しません。
網羅的チェックがその解決策です。これにより、コンパイラは単なるスペルチェッカーではなく、あなたのレビュー担当者になります。
never型の実践TypeScriptのnever型は、決して発生しない値を表します。常に例外をスローする関数はneverを返します。網羅的チェックのパターンはこれを悪用します。TypeScriptがブランチがnever型であると考えているのに、その結果を別のものに割り当てようとすると、毎回エラーが発生します。
これがコアパターンです。
type Status = "idle" | "loading" | "success" | "error";
function renderStatus(status: Status): string {
switch (status) {
case "idle":
return "Waiting...";
case "loading":
return "Loading...";
case "success":
return "Done.";
case "error":
return "Something went wrong.";
default:
// The exhaustiveness check
const _exhaustive: never = status;
return _exhaustive;
}
}
The assignment const _exhaustive: never = status is unreachable by design. If you add a new Status member and forget to add a case above, TypeScript assigns "new-status" to _exhaustive and immediately reports:
Type '"new-status"' is not assignable to type 'never'.
It does this at compile time. No tests needed for this particular bug category.
type ApiResponse<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: ApiError };
function showResponse<T>(response: ApiResponse<T>) {
switch (response.status) {
case "idle":
return <EmptyState />;
case "loading":
return <Spinner />;
case "success":
return <DataView data={response.data} />;
case "error":
return <ErrorBanner message={response.error.message} />;
default:
const _exhaustive: never = response;
return _exhaustive;
}
}
各ブランチは識別された共用体を正しく絞り込みます。もし誰かが後で"retry"をApiResponseに追加し、case "retry"ブランチの追加を忘れた場合、defaultケースはその正確な行で壊れます。ランタイムでの予期せぬ事態は起こりません。
多くのコードベースでは、インライン変数宣言よりも名前付き関数を好みます。読みやすく、問題が発生したときに検査する場所が1つにまとまります。
function assertNever(
x: never,
message = "Unexpected exhaustive case"
): never {
throw new Error(`${message}: ${JSON.stringify(x)}`);
}
Usage:
function processState(state: AppState) {
switch (state.type) {
case "init": return init(state);
case "fetch": return fetchData(state);
case "render": return render(state);
default: return assertNever(state, "Unhandled AppState");
}
}
関数の戻り値の型はneverなので、stateが実際にneverでない場合、TypeScriptはその呼び出しのコンパイルを拒否します。この拒否がバグのシグナルです。
素朴なアプローチは、defaultからneverチェックなしで例外をスローすることです。
default:
throw new Error(`Unhandled status: ${status}`);
これは似ているように見えますが、決定的なギャップがあります。新しいStatus値があっても黙ってコンパイルされます。ランタイムエラーは、そのパスが実際に本番環境でヒットしたときにのみ発生します。網羅的チェックは、誰かがデプロイする前に、コンパイル時に発生します。
| Aspect | Naive default throw | Exhaustive never check |
|---|---|---|
| Adding a new case | Compiles silently | Fails at compile time |
| Error detection | Surfaces at runtime in prod | Surfaces in editor, before commit |
| Type evolution | Easy to forget | Enforced by type system |
| IDE support | No signal | Highlights exact line |
網羅的チェックは、大規模な識別された共用体で最も輝きます。ステートマシン、ASTノード、イベントペイロードが一般的なターゲットです。
type AuthEvent =
| { kind: "login"; username: string }
| { kind: "logout" }
| { kind: "token_refresh"; token: string }
| { kind: "session_expired"; reason: string };
function logAuthEvent(event: AuthEvent) {
switch (event.kind) {
case "login":
console.log(`${event.username} logged in`);
break;
case "logout":
console.log("User logged out");
break;
case "token_refresh":
console.log(`Token refreshed, ends ${event.token.slice(-4)}`);
break;
case "session_expired":
console.log(`Session expired: ${event.reason}`);
break;
default:
const _exhaustive: never = event;
return _exhaustive;
}
}
Adding { kind: "mfa_challenge" } without a corresponding case breaks compilation immediately. This is the behavior you want in security-sensitive code where silently ignoring a case can mean silently bypassing a check.
if/else Chainsfunction getRoleName(role: UserRole): string {
if (role === "admin") return "Administrator";
if (role === "editor") return "Content Editor";
if (role === "viewer") return "Read-only Viewer";
// Exhaustiveness check at the end
const _exhaustive: never = role;
return _exhaustive;
}
このパターンはif/elseチェーンでも同じように機能します。チェーンの最後に「これ以上ここに到達するものはないはずだ」という番兵として自然に読み取れます。
never代入チェックは初期バージョンから安定しています。neverであることをコンパイラが強制することに依存します。厳格モードなしでも機能しますが、noImplicitAnyオフモードではシグナルが弱くなります。@typescript-eslint/switch-exhaustiveness-checkルールは、手動のnever変数なしで、ほとんどの識別された共用体のケースをカバーします。利用可能な場合はESLintルールを優先し、古いツールチェーンでは手動パターンを保持してください。never代入トリックは、Mapコールバックや三項演算子内では直接機能しません。これらのコンテキストでは名前付きヘルパー関数を優先してください。switchがマップされた型内の共用体をカバーする場合、網羅的チェックはプロパティアクセスで偽陽性を生成する可能性があります。エラーメッセージを読みやすくするために、チェックを別の関数に分割してください。switchステートメントを持つモノレポでのみ重要です。一般的なコードベースで網羅的チェックを最も効果的に適用できる場所:
// インラインバージョン(最も一般的)
function handleStatus(status: Status): string {
switch (status) {
case "idle": return "Waiting";
case "loading": return "Loading...";
case "success": return "Done";
case "error": return "Failed";
default: { const _exhaustive: never = status; return _exhaustive; } }
}
// ヘルパー関数バージョン(大規模なスイッチでよりクリーン)
function assertNever(x: never, msg?: string): never {
throw new Error(msg ?? `Unhandled case: ${JSON.stringify(x)}`);
}
// 識別された共用体を使用
type Tree =
| { type: "leaf"; color: string }
| { type: "branch"; children: Tree[] }
| { type: "fruit"; kind: string };
function describeTree(tree: Tree): string {
switch (tree.type) {
case "leaf": return `Leaf: ${tree.color}`;
case "branch":
return `Branch with ${tree.children.length} children`;
case "fruit": return `Fruit: ${tree.kind}`;
default: return assertNever(tree);
}
}
あるチームが重要な金融アプリケーションを開発しています。彼らはトランザクションステータスを処理するために`never`型を使用した網羅的なチェックを実装しました。新しいトランザクションステータス「'pending_review'」が`TransactionStatus`ユニオン型に追加されました。開発者が`TransactionStatus`を処理する既存の`switch`ステートメントを更新し忘れた場合、アプリケーションのコンパイルとランタイムの動作にどのような即時的な影響があるか、最も適切に説明しているのは次のうちどれですか?
AI CLIログの整形、Cron式の作成、JWTの解析、chmod権限の計算をオフラインで実行。

大規模なPythonコードベースで、ParamSpec、TypeVarTuple、Protocols、カスタム型ガードを用いた高度なmypy strict型チェックパターンを習得しましょう。
続きを読む
branded type、条件付き応答型、テンプレートリテラルルーティング、satisfies演算子など、エンタープライズ向けTypeScriptの高度なパターンを習得しましょう。
続きを読む
条件型、マップ型、テンプレートリテラル型、branded type、判別共用体など、TypeScriptジェネリクスの高度なパターンを習得し、型安全なプロダクションライブラリを構築しましょう。
続きを読む