【入門編】共用体型(Union Types)の絞り込み:判別可能な共用体(Discriminated Unions)の設計パターン – TypeScript コア・型システムの基礎解析バイブル

こんにちは、TypeScriptの世界へようこそ。私はこれまで数多くの大規模プロジェクトでアーキテクチャを設計し、TypeScriptの型システムが持つ真の力をプロダクトに注ぎ込んできました。

今日は、初心者の方がTypeScriptを学ぶ上で最初にぶつかる壁であり、かつ「これをマスターすれば、あなたのコードの安全性は10倍跳躍する」と断言できる重要なテーマをお話しします。

それが、「判別可能な共用体(Discriminated Unions)」です。

「難しそうな名前だな」と身構える必要はありません。これは、TypeScriptが私たちの書いたコードをどう解釈し、どう守ってくれるかを知るための、とても優しく、そして強力な武器なのです。さあ、一緒にその扉を開いてみましょう。

—

1. なぜ「型の絞り込み」が必要なのか?

TypeScriptの最大の特徴は「型があること」ですが、実際の開発では「複数の可能性がある状態」を扱わなければなりません。

例えば、Webアプリでサーバーからデータを取得するシーンを想像してください。

  • 通信が成功して、データが届いた状態
  • 通信が失敗して、エラーメッセージがある状態
  • 通信中で、まだ何も手元にない状態

これらを一つのオブジェクトで表現しようとすると、つい「全部入りの型」を作ってしまいがちです。

// ⚠️ あまり良くない設計の例
interface ResponseData {
status: string; // “success” | “error” | “loading”
data?: string[]; // 成功時のみ存在
errorMessage?: string; // 失敗時のみ存在
}

function handleResponse(res: ResponseData) {
// ここで res.data を使おうとすると…
// TypeScriptは「dataはundefinedかもしれないよ!」と警告します。
// なぜなら、statusが “error” の時もこの型は成立してしまうからです。
console.log(res.data.length); // ❌ エラー!
}

このように、「あるプロパティがあるときは、こっちのプロパティはない」という矛盾した状態を許容してしまうと、コードは一気に不安定になります。これを解決するのが「判別可能な共用体」です。

—

2. 「判別可能な共用体」の3つの条件

このパターンを成立させるには、3つの材料を揃えるだけです。

1. 共通の「タグ(名前)」を持っていること:すべての型に同じ名前のプロパティ(例:`type`や`kind`)を持たせます。
2. そのタグが「リテラル型」であること:単なる `string` ではなく、`”success”` や `”error”` といった具体的な値そのものを型にします。
3. それらを「共用体(Union)」で結ぶこと:`type Result = A | B` のようにまとめます。

具体的なコードで見てみましょう。

// 1. 各状態を個別のインターフェースとして定義する
interface Success {
type: ‘success’; // これが「リテラル型」のタグ
data: string[];
}

interface Failure {
type: ‘error’; // 同じくタグ。値が違うのがポイント
error: Error;
}

interface Loading {
type: ‘loading’;
}

// 2. それらを共用体(Union)で合成する
type ApiResponse = Success | Failure | Loading;

—

3. コンパイラを味方につける「絞り込み」の魔法

ここからがTypeScriptの真骨頂です。この `ApiResponse` を使う関数を書いてみましょう。

function processResponse(res: ApiResponse) {
// この時点では、res は Success か Failure か Loading か分かりません。

switch (res.type) {
case ‘success’:
// ✨ ここが魔法!
// TypeScriptは「typeが’success’なら、これはSuccess型だ」と確信します。
// なぜなら、他の型(Failure, Loading)の type は ‘success’ ではないからです。
console.log(“データ取得成功!”, res.data); // ✅ 安全にアクセスできる
break;

case ‘error’:
// ここでは自動的に Failure 型として扱われる
console.error(“エラー発生:”, res.error.message); // ✅ 安全
break;

case ‘loading’:
console.log(“読み込み中です…”);
break;
}
}

この 「`if` や `switch` でチェックした瞬間に、そのブロック内での型が確定する」 仕組みを、私たちは 「型ガード(Type Guard)による絞り込み(Narrowing)」 と呼びます。

開発者が `as Success` といった「型アサーション(強制的な型変換)」を使わなくても、TypeScript自身が論理的に「これしかない!」と導き出してくれる。これが世界最高峰の型システムの知性なのです。

—

4. 陥りやすい罠:タグを忘れないで

初学者がよくやってしまうミスは、タグを共通化し忘れたり、広い型(stringなど)にしてしまうことです。

// ❌ 失敗例:タグが共通化されていない
interface Success { successData: string[] }
interface Failure { errorData: string }

type Result = Success | Failure;

function handle(res: Result) {
// 「どっちの型か」を判定する共通の手がかりがないため、
// 結局 if (“successData” in res) のような、少し不格好な判定が必要になります。
}

必ず、「同じ名前のプロパティ(`type` など)」 を持たせるようにしましょう。それが型システムにとっての「名札」になります。

—

5. 【極限の知見】「網羅性チェック」で未来のバグを防ぐ

最後に、プロの現場で必ず使われるテクニックをご紹介します。
もし将来、`ApiResponse` に新しい状態 `”timeout”` が追加されたとしたらどうでしょう? `switch` 文に `case ‘timeout’` を書き忘れると、バグになりますよね。

TypeScriptには、これをコンパイルエラーとして検出する方法があります。

function processResponse(res: ApiResponse) {
switch (res.type) {
case ‘success’: / … / break;
case ‘error’: / … / break;
case ‘loading’: / … / break;

default:
// もし全てのケースを網羅していれば、ここでの res は「never型(ありえない型)」になります。
// もし網羅していなければ、ここに漏れた型が入り込み、エラーが発生します。
const _exhaustiveCheck: never = res;
return _exhaustiveCheck;
}
}

この `never` を使ったチェック(Exhaustiveness Check)を仕込んでおけば、型定義を増やした瞬間に「処理が足りないよ!」とコンパイラが教えてくれます。これこそが、人間がミスをすることを前提に、システムでそれをカバーする「掌握」の技術です。

—

まとめ:TypeScriptの基本はバッチリマスターできます!

「判別可能な共用体」は、単なる文法ではありません。「実行前に、起こりうるすべての状態をコード上で証明する」 というTypeScriptの哲学そのものです。

1. タグ(Literal Type)を付ける
2. Union(|)でつなぐ
3. 絞り込み(Narrowing)で使う

この3ステップを意識するだけで、あなたの書くコードは驚くほど堅牢で、読みやすく、そして美しいものに変わります。

最初は少し戸惑うかもしれませんが、大丈夫。TypeScriptはあなたの意図を理解しようと常に背後で動いています。エラーが出たら、それはコンパイラがあなたを守ろうとしているサインです。

一歩ずつ、この強力な型システムを自分の手足のように馴染ませていきましょう。応援していますよ!

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