TypeScriptの深淵:`unknown`と`never`で型安全の聖域を構築する
フロントエンドの現場において、`any`は「思考停止」の代名詞だ。しかし、`any`を排除した先に待ち受けるのは、型システム特有の厳格な壁である。特に`unknown`(トップ型)と`never`(ボトム型)の扱いは、単なる言語仕様の理解を超え、「コンパイラに何を保証させるか」という設計思想そのものだ。
今日は、インターフェース設計の現場でこれらをどう武器にすべきか、その極意を伝授しよう。
—
1. `unknown`は「信頼の担保」である
`unknown`は、TypeScriptにおける「すべての型の親」だ。しかし、`any`と決定的に違うのは、「アクセスする前に型を絞り込め(Type Guard)」というコンパイラの強烈な要求だ。
APIレスポンスの型定義をサボって`any`に逃げている諸君、それは技術的負債の先送りだ。`unknown`こそが、境界線(Boundary)での正しい防衛線となる。
実践:型安全なAPIクライアントの設計
interface ApiResponse
data: T;
status: number;
}
// レスポンスが未知の状態で入ってきたとき、anyではなくunknownを使う
async function fetchUser(): Promise
const response = await fetch(‘/api/user’);
return response.json();
}
// 型ガードを介して初めて安全に利用可能にする
function isUser(data: unknown): data is { id: number; name: string } {
return typeof data === ‘object’ && data !== null && ‘id’ in data;
}
const rawData = await fetchUser();
if (isUser(rawData)) {
// ここでは rawData は { id: number; name: string } に絞り込まれる
console.log(rawData.name);
} else {
throw new Error(‘Invalid response structure’);
}
リードの視点: ここで重要なのは、「外部からのデータは常に汚染されている」という前提だ。`unknown`を使うことで、フロントエンドのコンポーネントに渡る前に「正しい型であることを強制的に検証」させるフローが生まれる。これが堅牢なアーキテクチャの第一歩だ。
—
2. `never`は「あり得ない」をコード化する
`never`はTypeScriptのボトム型であり、「決して存在しない値」を意味する。多くの開発者は`never`を関数の戻り値(throwなど)でしか見ていないが、真の使い道は「網羅性チェック(Exhaustive Check)」にある。
実践:switch文の網羅性チェック
type Action = { type: ‘ADD’ } | { type: ‘REMOVE’ };
function reducer(action: Action) {
switch (action.type) {
case ‘ADD’:
return;
case ‘REMOVE’:
return;
default:
// ここに到達したとき、actionは自動的に never 型になる
const _exhaustiveCheck: never = action;
return _exhaustiveCheck;
}
}
リードの視点: もし後から `type: ‘UPDATE’` を追加したのに、`reducer`の`switch`文を更新し忘れたらどうなるか? TypeScriptコンパイラが「`{ type: ‘UPDATE’ }` 型を `never` 型に割り当てることはできません」と即座にエラーを吐く。これはテストコードを100行書くよりも強力な「型による仕様保護」だ。
—
3. インターフェース設計のアンチパターン:なぜ `never` はインターフェースに入れないのか
たまに、プロパティを「絶対に使わせたくない」という意図で `prop: never` とするコードを見かけるが、これは罠だ。
interface User {
id: number;
adminFlag: never; // 危険な記述
}
これをやってしまうと、そのインターフェースを実装する側は、`adminFlag` に何を代入してもコンパイルエラーになる。一見すると「封印」に成功しているように見えるが、型推論の破壊を招く。
真の解決策:
プロパティを隠蔽したい、あるいは条件付きで存在させたい場合は、`never`による封印ではなく、「Discriminated Unions(判別可能なユニオン型)」を使うべきだ。
interface Guest {
type: ‘guest’;
}
interface Admin {
type: ‘admin’;
adminFlag: true;
}
type User = Guest | Admin;
このように、型そのものを分けることで、「Adminでなければ `adminFlag` は存在しない」という論理をコンパイラに理解させる。これがTypeScriptの王道だ。
—
結論:コードは「読み手」ではなく「コンパイラ」のために書く
優れたフロントエンドエンジニアは、コードの読みやすさだけでなく、「コンパイラがどの程度我々の意図を理解できているか」を常に意識している。
- `unknown`: 境界線で使い、入力を疑え。
- `never`: ロジックの終端で使い、網羅性を保証せよ。
この二つを使いこなすことで、ランタイムエラーの大部分は開発環境のコンパイル時に消滅する。複雑なライブラリを導入する前に、まずはこの「型による厳密な証明」を日々のコードに取り入れてみてほしい。それが、伝説的なアーキテクトへの第一歩だ。
コードは、書くものではない。証明するものだ。