おい、最近TypeScript書いてて「この `any` とか `as` のキャスト、そろそろ誰何(すいか)される頃合いだな……」なんて冷や汗かいてないか?
気持ちはよく分かる。APIから飛んでくるレスポンスや、複雑なUIの状態管理(ReduxのreducerやZustandのストアなど)を実装していると、どうしても「このオブジェクト、今はこういう状態だから、このプロパティが存在するはずなんだ!」っていう文脈が型にうまく伝わらなくて、ついつい力技で逃げたくなる瞬間があるよな。
でも、ちょっと待て。その場しのぎの `as` キャストでコンパイラを黙らせるの、今日で終わりにしようぜ。
今回は、中級からワンランク上の「真のTypeScript使い」へステップアップするための必須教養、「判別可能なユニオン型(Discriminated Unions)」について、俺が現場のリアルな知見を交えて徹底的に叩き込んでやる。
しっかりついてこいよ。
—
なぜ `as` キャストや `any` は現場の爆弾になるのか?
実務でフロントエンドを書いていて一番恐ろしいのは、コンパイルが通ったのに本番環境(ブラウザ上)で突然 `TypeError: Cannot read properties of undefined` が爆発する瞬間だ。
例えば、サーバーから取得するユーザーのデータや、非同期処理のステータス(ローディング中、成功、エラー)を次のようなコードで表現していないか?
// 良くない例:ただのオプショナルプロパティの寄せ集め
interface State {
isLoading: boolean;
data?: { id: string; name: string };
error?: Error;
}
この型定義、一見するとシンプルで良さそうに見えるだろ? だが、これだと「`isLoading` が `true` なのに `data` が存在する」という、論理的にあり得ない矛盾した状態すらTypeScriptは許容してしまう。
結果として、コンポーネント側で `state.data.name` にアクセスするときに、毎回 `if (state.data)` とか `?.` (オプショナルチェイニング)でガードしなきゃいけない。挙句の果てに、自信満々に `state.data!.name` なんて非nullアサーションを書いた日には、未来のバグ製造マシーンの完成だ。
型安全とは、「不正な状態を型レベルで表現不能にする」こと。これを実現するのが「判別可能なユニオン型」だ。
—
判別可能なユニオン型(Discriminated Unions)の基本構造
仕組みは拍子抜けするほどシンプルだ。要するに、すべてのオブジェクト型に「共通の識別子(タグ)」を持たせる。それだけ。
百聞は一見にしかず。まずは基本のコードを見てみよう。
// — 1. それぞれの状態を独立したインターフェースで定義する —
interface LoadingState {
status: ‘loading’; // ←これが「タグ(判別子)」!
}
interface SuccessState {
status: ‘success’; // ←タグ
data: { id: string; name: string }; // 成功時に絶対必要なデータ
}
interface ErrorState {
status: ‘error’; // ←タグ
error: Error; // エラー時に絶対必要なエラーオブジェクト
}
// — 2. それらをユニオン型でまとめる —
type ApiState = LoadingState | SuccessState | ErrorState;
見てくれ、この美しさを。`status` という共通のプロパティ(リテラル型)が、それぞれの状態を完全に分離している。
ローディング中なら `data` なんて存在しないし、エラーの時に `data` が混ざることも絶対にない。
—
ブラウザの裏側でTypeScriptはどう処理しているか?(型ガードのからくり)
「で、どうやって使うの? `if` で分岐させるだけ?」と思ったそこの君。大正解だ。
しかし、ただの `if` じゃない。TypeScriptのコンパイラ(tsc)は、このコードをビルドする際、またはエディタで解析する際に、「制御フロー分析(Control Flow Analysis)」という強力な最適化を行っている。
TypeScriptの心臓部である型チェッカーは、次のようなコードに出会ったとき、裏側でどう動いているか知っているか?
function handleResponse(state: ApiState) {
// ここでは state は LoadingState | SuccessState | ErrorState のどれか分からない
switch (state.status) {
case ‘loading’:
// ここに入った瞬間、TypeScriptは「あ、こいつの status は ‘loading’ だな」と推論する。
// つまり、このブロック内では state は強制的に LoadingState に絞り込まれる(Narrowing)。
console.log(‘読み込み中…’);
break;
case ‘success’:
// ここでは SuccessState に絞り込まれるため、
// IDEの補完が効き、安全に .data にアクセスできる!
console.log(`成功: ${state.data.name}`);
break;
case ‘error’:
// ここでは ErrorState に絞り込まれる
console.error(state.error.message);
break;
}
}
ブラウザの実行時(JavaScriptにトランスパイルされた後)には、単なる文字列の比較(`state.status === ‘success’` など)にコンパイルされるだけだから、ランタイムのオーバーヘッドはゼロ。
それでいて、開発時には静的解析によってバグを完全にシャットアウトできる。これぞTypeScriptの真骨頂だ。
—
現場で即コピペできる!実践的なデザインパターン
さて、理論はこれくらいにして、明日からの実務でそのまま使える「ちょっと進んだテクニック」を授けよう。
1. アクションのディスパッチ(ReduxやZustand風)
フロントエンドで最も恩恵を受けるのは、状態管理のReducerや、フォームのステップ管理などの「イベント駆動型の処理」だ。
// フォームのステップ管理を型安全に実装する例
type StepAction =
| { type: ‘NEXT_STEP’ }
| { type: ‘GO_TO_STEP’; payload: { stepNumber: number } }
| { type: ‘UPDATE_INPUT’; payload: { field: string; value: string } }
| { type: ‘RESET’ };
function stepReducer(currentStep: number, action: StepAction): number {
switch (action.type) {
case ‘NEXT_STEP’:
return currentStep + 1;
case ‘GO_TO_STEP’:
// action.payload が確実に存在し、型推論される
return action.payload.stepNumber;
case ‘UPDATE_INPUT’:
// ここでバリデーションや値の保存処理を行う
console.log(`${action.payload.field} を ${action.payload.value} に更新`);
return currentStep;
case ‘RESET’:
return 1;
default:
// 【プロの技】網羅性チェック(Exhaustiveness Check)
// 将来 action が増えて処理を書き忘れた場合、ここでコンパイルエラーにしてくれる
const _exhaustiveCheck: never = action;
return _exhaustiveCheck;
}
}
この `default` 句の中にある `const _exhaustiveCheck: never = action;`、これめちゃくちゃ重要だから覚えておいてくれ。
もし将来、新しいアクション(例: `{ type: ‘SUBMIT’ }`)を追加したのに `switch` ケースを書き忘れた場合、TypeScriptが「おい、`action` がまだ `never` に絞り込めてねぇぞ!」とコンパイルエラーで教えてくれる。人間のうっかりミスを型システムに防がせる最高のエコシステムだ。
—
シニアからのアドバイス:設計時の心構え
判別可能なユニオン型を使いこなせるようになると、コードから無駄な `undefined` チェックや `as` キャストが驚くほど消え去る。コードベースのメンテナビリティが跳ね上がり、チームメンバーからの信頼も厚くなるはずだ。
ただ、一つだけ注意してほしい。
「とりあえず何でもかんでもユニオンにすればいいや」とタグを乱立させると、今度は型定義自体が肥大化してメンテが辛くなる。「状態によって持っているべきプロパティが明確に異なる(排他的である)」という要件を見極めた上で、このパターンを適用してほしい。
明日からのコードレビューで、誰かの無意味な `as` キャストを見つけたら、「ここ、判別可能なユニオン型でいけるんじゃね?」とクールにアドバイスしてやってくれ。
それじゃ、今日も最高のコードを書こうぜ!

コメント