TypeScriptの型システムを「武器」にする:判別可能な共用体による状態管理の極致
多くの開発者が `interface` と `type` を単なるデータ構造の定義として使っていますが、それはTypeScriptの真の力を半分も引き出せていません。
特に、非同期通信やUIの状態遷移を扱う際、安易な `optional` プロパティの羅列(`data?: T; error?: E; isLoading?: boolean;`)で設計していませんか? それこそが、実行時に「ありえない状態」を生み出し、バグを招く最大の要因です。
今日は、コンパイラの型推論を最大限に活用し、状態遷移を「型」として固定する「判別可能な共用体(Discriminated Unions)」の極意を伝授します。
—
なぜ「オプショナル」の羅列は悪なのか?
例えば、API通信の状態を以下のように定義したとします。
// 悪い設計の典型例
interface State {
data?: User;
error?: Error;
isLoading: boolean;
}
この定義では、`isLoading` が `false` で `data` が `undefined` のとき、アプリは「通信が終わっているのにデータがない」という論理的にありえない状態を許容してしまいます。これを確認するために、コードの至る所で `if (state.data)` といったガード節を書き散らすことになります。これは疲弊の始まりです。
—
判別可能な共用体による「状態の封じ込め」
状態を「排他的」に定義することで、コンパイラに「今、どの状態にあるか」を確実に理解させます。
// 状態を型で厳密に切り分ける
type RemoteData
| { type: ‘IDLE’ }
| { type: ‘LOADING’ }
| { type: ‘SUCCESS’; data: T }
| { type: ‘FAILURE’; error: Error };
// 利用側の実装:状態遷移を型安全にハンドリングする
function renderUI
switch (state.type) {
case ‘IDLE’:
return ‘待機中…’;
case ‘LOADING’:
return ‘読み込み中…’;
case ‘SUCCESS’:
// ここで自動的に state.data にアクセス可能になる
return `データ: ${state.data}`;
case ‘FAILURE’:
// ここで自動的に state.error にアクセス可能になる
return `エラー: ${state.error.message}`;
default:
// 網羅性チェック(Exhaustive Check)
const _exhaustiveCheck: never = state;
return _exhaustiveCheck;
}
}
ここが技術の肝:
1. 排他性: `SUCCESS` 型のときには `error` プロパティは存在し得ません。IDEの補完も効き、不要なプロパティへのアクセスは型エラーになります。
2. 網羅性チェック: もし将来、新しい状態 `RETRYING` を追加した場合、`switch` 文に漏れがあれば `default` 節で `never` 型への代入エラーが発生します。コードを修正すべき場所がコンパイル時に確定するという体験は、大規模開発において最強の武器となります。
—
実務で差がつく:判別可能な共用体の「一歩先」
実務の現場では、コンポーネントのPropsやRedux/Zustandのストア設計にこれを応用します。さらに高度なテクニックとして、「識別子を抽象化する型ユーティリティ」を導入することをお勧めします。
/
- 識別子を統一して管理するためのユーティリティ
/
type DiscriminatedUnion
// 状態遷移の定義を構造化する
type UserState = DiscriminatedUnion<
| { type: 'UNAUTHORIZED' }
| { type: 'AUTHORIZED'; user: User; permissions: string[] }
| { type: 'SUSPENDED'; reason: string }
>;
このように、型定義を `type` エイリアスで集中管理することで、プロジェクト全体のドキュメントとしても機能させます。
—
パフォーマンスとコンパイル速度への配慮
「型を複雑にするとコンパイルが遅くなるのでは?」という懸念を持つ方もいるでしょう。確かに巨大な再帰的型はコンパイル負荷が高いですが、このような共用体パターンはTypeScriptコンパイラにとって非常に最適化しやすい構造です。
- 型推論の高速化: 共用体の判別(Discrimination)は、コンパイラが最も得意とする処理の一つです。
- メモリ効率: 実行時にはただのオブジェクトとして扱われるため、実行時のオーバーヘッドは皆無です。
—
結論:型は「守り」ではなく「攻め」の設計図
フロントエンド開発において、バグの大部分は「データの不整合」から生まれます。`if` 文による泥臭いチェックを繰り返すのではなく、「型によって不正な状態を作れないようにする」ことが、プロフェッショナルなフロントエンドエンジニアの仕事です。
明日からのコードレビューで、もし `if` や `optional` の山を見つけたら、こう言ってあげてください。
「その状態、型で封印(Discriminated)できるよ」
この設計思想を取り入れるだけで、あなたのプロダクトの堅牢性は一段上の次元へ引き上げられます。型システムを心から信頼し、コンパイラをあなたの最強のペアプログラマーにしてください。