【実務・中級編】TypeScriptの「トップ型(unknown)」と「ボトム型(never)」をInterfaceで扱う際の注意点 – TypeScript コア・型システムの基礎解析バイブル

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`: ロジックの終端で使い、網羅性を保証せよ。

この二つを使いこなすことで、ランタイムエラーの大部分は開発環境のコンパイル時に消滅する。複雑なライブラリを導入する前に、まずはこの「型による厳密な証明」を日々のコードに取り入れてみてほしい。それが、伝説的なアーキテクトへの第一歩だ。

コードは、書くものではない。証明するものだ。

タイトルとURLをコピーしました