TypeScriptの「境界」を制する:unknownとneverで実現する堅牢なデータバリデーション
TypeScriptの型システムは、コンパイル時にのみ存在する「幻」ではありません。それは、ランタイムの不確実性と我々のコードとの間に引かれた、唯一の信頼の境界線です。
多くのエンジニアが「なんとなく」で `any` を使い、あるいはバリデーションを疎かにしてランタイムエラーを量産しています。今日は、TypeScriptの型システムの端点である「トップ型(`unknown`)」と「ボトム型(`never`)」をInterfaceと組み合わせ、外部入力を安全に型付けするアーキテクチャについて語ります。
—
1. なぜ unknown を使うのか? その「重み」を知る
`any` は型チェックを無効化する「降伏」の印です。一方で `unknown` は、「このデータが何者であるか、まだ証明されていない」という誠実な宣言です。
外部APIやフォーム入力から受け取ったデータは、TypeScriptの型システムから見れば「砂」です。我々はその砂の中から、特定のインターフェースという「鋳型」に合う部分だけを抽出する必要があります。
アンチパターン:安易なキャスト
// 悪い例:型アサーションはコンパイラへの嘘
const data = JSON.parse(response) as User;
// 実行時に構造が違えば、以降の処理はすべて未定義動作(undefined)に直面する
—
2. 堅牢なバリデーションのための「ユーザー定義型ガード」
コンパイル時に型を確定させるためには、ランタイムでのチェックが必要です。ここで `never` が重要な役割を果たします。
以下は、外部データが期待する `User` インターフェースを満たしているかを検証する、プロダクションコードのテンプレートです。
interface User {
id: string;
email: string;
role: ‘admin’ | ‘user’;
}
/
- ユーザー定義型ガード
- unknown を受け取り、成功すれば User を返す
/
function isUser(data: unknown): data is User {
// 1. null 判定とオブジェクト型判定
if (typeof data !== ‘object’ || data === null) return false;
// 2. 構造のバリデーション
const u = data as Record
return (
typeof u.id === ‘string’ &&
typeof u.email === ‘string’ &&
(u.role === ‘admin’ || u.role === ‘user’)
);
}
—
3. never を活用した「網羅性チェック」の極意
`never` 型は、「決して到達しない」ことを意味します。この性質は、switch文やif文で「全てのケースを処理したか」を保証するために使います。
function handleRole(role: User[‘role’]) {
switch (role) {
case ‘admin’:
return console.log(“管理者権限”);
case ‘user’:
return console.log(“一般権限”);
default:
// ここに到達することはありえない(never)
const _exhaustiveCheck: never = role;
return _exhaustiveCheck;
}
}
このパターンを仕込んでおけば、将来 `User` 型に `’guest’` が追加された瞬間、コンパイラが「`handleRole` の `default` 節に到達可能である」とエラーを吐き出し、修正漏れを未然に防いでくれます。
—
4. 実戦:バリデーション失敗時の「安全な処理」
実務では、バリデーションに失敗した際に「何が足りなかったのか」を特定する必要があります。ここで、`never` の性質を応用した、型安全なエラーハンドリングを紹介します。
type ValidationResult
| { success: true; data: T }
| { success: false; error: string };
function validateUser(data: unknown): ValidationResult
if (isUser(data)) {
return { success: true, data };
}
// ここで初めて、型が適合しない場合の処理を行う
return { success: false, error: “Invalid User Structure” };
}
// 利用側
const input = await fetch(‘/api/user’).then(res => res.json());
const result = validateUser(input);
if (result.success) {
// ここでは型が確定しているため、安全にアクセス可能
console.log(result.data.email);
} else {
// 型システムの境界を超えた場所で、初めてランタイムのエラーハンドリングを行う
console.error(result.error);
}
—
チーフアーキテクトからの助言
プロダクションコードにおけるバグの9割は、「外部からの入力を、内部の型定義が正しいと信じ込んでしまったこと」に起因します。
1. `unknown` を入り口にする: 境界線で止めて、型チェックを行う。
2. `is` 述語で型を絞り込む: 型ガード関数を型定義の近くに配置する。
3. `never` で網羅性を担保する: 将来のコード修正時に、型システム自体をテストスイートのように働かせる。
この3ステップを徹底するだけで、あなたのフロントエンドは驚くほど堅牢になります。「なんとなく動く」コードから、「コンパイラが保証する」コードへ。それが、プロフェッショナルの仕事です。
次は、`Zod` などのバリデーションライブラリと、今回の `Interface` をどう統合してコード量を減らしていくかについて深掘りしましょう。型システムを愛する者たちよ、健闘を祈ります。