ユニオン型を使い始めると、いつかこの壁にぶつかります。「オブジェクト型が増えてきたら、どうやって絞り込めばいいんだろう?」

Discriminated Unionって何?ユニオン型とは違うの?
コードで「kind」とか「type」プロパティが出てきたけど、これって何のためにあるんだろう
switch文と型の絞り込みを組み合わせる書き方がよくわからない

この記事では、Discriminated Union(タグ付きユニオン)とは何か、なぜ必要なのか、どう書くのかを順番に解説します。「型に目印(タグ)を付けることで、型を安全に絞り込める」というパターンが核心です。

この記事は次のような方におすすめです。

この記事はこんな人におすすめ!
  • TypeScriptのユニオン型と型ガードの基本は理解している
  • 実際のコードでDiscriminated Unionが出てきて意味がわからない
  • 複数のオブジェクト型を扱うとき、型の絞り込みが煩雑になってきた
  • switch文と型の絞り込みを組み合わせた書き方を知りたい

この記事を読むと、Discriminated Unionの概念・書き方・使いどころを一通り理解でき、「型で状態を表現する」という考え方が身につきます。

それでは、順を追って詳しく見ていきましょう!

通常のユニオン型で困る場面 — 絞り込みが複雑になるとき

ユニオン型はとても便利な機能ですが、複数のオブジェクト型を扱うときに少し悩ましいことが起きます。

複数のオブジェクト型が混在するユニオンはifが増えやすい

たとえば、図形を表す型を考えてみましょう。円(Circle)と四角形(Square)があって、それぞれ面積を計算したいとします。

type Circle = {
  radius: number;
};

type Square = {
  side: number;
};

type Shape = Circle | Square;

この Shape 型の値を受け取って面積を計算する関数を書こうとすると……

function getArea(shape: Shape): number {
  // radiusがあれば円、sideがあれば四角形?
  if ("radius" in shape) {
    return Math.PI * shape.radius ** 2;
  } else {
    return shape.side ** 2;
  }
}

"radius" in shape という書き方で絞り込んでいます。今は2種類なのでまだ読めますが、図形の種類が5種類、10種類と増えてくると、この in チェックを並べた分岐がどんどん増えていきます。

型ガードだけで解決しようとすると何が辛いのか

型ガードを使えば解決できそうに思えますが、オブジェクト型に typeof を使うと結果は常に "object" になります。Circle なのか Square なのかを typeof では区別できないのです。

instanceof を使う手もありますが、そのためには class で定義し直す必要があります。シンプルなオブジェクト型のユニオンに対して class を使うのは、少し大げさになることもあります。

「オブジェクト型のユニオンを、もっとすっきり絞り込める方法はないか」、そのためのパターンが Discriminated Union です。

なかむぅ
なかむぅ
型ガードまわりの詳しい仕組みについては、別の記事で解説しています。
TypeScriptのユニオン(Union)型とは?型ガードで「どの型か」を安全に絞り込む方法
TypeScriptのユニオン(Union)型とは?型ガードで「どの型か」を安全に絞り込む方法TypeScriptのユニオン型とは何かを基礎から解説。typeof・in・instanceofを使った型ガードの違いと使い分けも初心者向けにわかりやすく整理します。...

Discriminated Unionとは — 「タグ」で型を識別するパターン

Discriminated Unionは、各オブジェクト型に「自分がどの型か」を示す共通のプロパティ(タグ)を持たせる設計パターンです。

「discriminated」は「区別された」という意味で、「union」はユニオン型のことです。直訳すると「区別可能なユニオン型」で、日本語では「判別可能なユニオン型」や「タグ付きユニオン」と呼ばれることもあります。

共通のリテラル型プロパティがポイント

Discriminated Union の核心は、すべての型に同じ名前のプロパティを持たせ、その値をリテラル型にすることです。

このプロパティのことを「discriminant(ディスクリミナント)」や「タグ」と呼びます。タグの値はそれぞれの型で異なるリテラル値にします。

よく使われるプロパティ名は kindtype ですが、名前はなんでもかまいません。

Discriminated Unionの基本的な書き方

先ほどの図形の例を、Discriminated Unionで書き直してみます。

type Circle = {
  kind: "circle";  // タグ(リテラル型)
  radius: number;
};

type Square = {
  kind: "square";  // タグ(リテラル型)
  side: number;
};

type Shape = Circle | Square;

ポイントは kind プロパティの値が、それぞれ "circle""square" というリテラル型になっている点です。この「タグ」がTypescriptに「今どちらの型か」を教える手がかりになります。

switch文でタグを使って型を安全に絞り込む

タグを持たせた Discriminated Union は、switch文と組み合わせることで真価を発揮します。

各ケースでプロパティが確定している状態を体感する

kind プロパティを switch で分岐すると、各 case の中で TypeScript が自動的に型を絞り込んでくれます。

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      // この中では shape は Circle 型として扱われる
      return Math.PI * shape.radius ** 2;
    case "square":
      // この中では shape は Square 型として扱われる
      return shape.side ** 2;
  }
}

case "circle": の中では shape.radius が確実に存在し、case "square": の中では shape.side が確実に存在します。TypeScript がタグを見て、その case の中で使えるプロパティを絞り込んでくれるのです。

これは補完も効きます。case "circle": の中で shape. と打つと、radius がサジェストされます。存在しない side にアクセスしようとするとエラーになります。

ifよりswitchが読みやすくなる理由

in 演算子を使ったifの分岐と比べると、switchを使ったDiscriminated Unionは「何で分岐しているか」が一目でわかります。

// Before: in演算子で分岐(型が増えると読みにくくなりやすい)
if ("radius" in shape) { ... }
else if ("side" in shape) { ... }

// After: タグで分岐(何を根拠に絞り込んでいるか明確)
switch (shape.kind) {
  case "circle": ...
  case "square": ...
}

「なぜこの分岐になるのか」がコードを読む人に伝わりやすくなるのが、Discriminated Union の大きなメリットです。

never型との組み合わせで「絞り込み漏れ」を防ぐ

Discriminated Union をさらに安全に使う方法として、never型を使った「網羅チェック」があります。

switch文に default ケースを追加し、そこで never 型のアサーションを使うと、全パターンを処理しているかどうかをコンパイル時に確認できます。

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.side ** 2;
    default:
      // ここに到達したら「型定義に漏れがある」という意味
      const _exhaustiveCheck: never = shape;
      throw new Error(`未対応の形: ${_exhaustiveCheck}`);
  }
}

Shape 型に新しい型(たとえば Triangle)を追加したとき、switch文の case"triangle" を追加し忘れると、defaultnever の箇所でコンパイルエラーが出ます。これにより「型を増やしたのに処理を追加し忘れた」というバグを事前に防げます。

なかむぅ
なかむぅ
never型の詳しい解説は別の記事で扱っています。Discriminated Union と組み合わせると特に力を発揮するので、興味があればあわせて読んでみてください。
TypeScriptのnever型 — 3つの登場シーンと使い所をわかりやすく解説TypeScriptを使っていると、ふとしたタイミングでコードの中に never という型が出てくることがあります。 ...

どんな場面で使うと便利か — 実践的な使いどころ

Discriminated Union は、「複数の状態があって、状態ごとに持つデータが違う」場面で特に役立ちます。

状態の種類を型で表現する(ローディング・成功・失敗など)

データを取得するときの状態管理はよくあるユースケースです。

type LoadingState = {
  status: "loading";
};

type SuccessState = {
  status: "success";
  data: string[];
};

type ErrorState = {
  status: "error";
  message: string;
};

type FetchState = LoadingState | SuccessState | ErrorState;

これを switch で分岐すると、各状態で必要なプロパティだけが安全に使えます。

function render(state: FetchState): string {
  switch (state.status) {
    case "loading":
      return "読み込み中...";
    case "success":
      return `取得件数: ${state.data.length}件`;  // data は確実に存在する
    case "error":
      return `エラー: ${state.message}`;          // message は確実に存在する
  }
}

ローディング中なのにデータを参照してしまう、といったミスをコンパイル時に防げるのが大きなメリットです。

APIレスポンスのパターン分類

APIのレスポンスが「成功」か「失敗」かでデータ構造が変わるときにも使えます。

type ApiSuccess = {
  result: "ok";
  body: { id: number; name: string };
};

type ApiError = {
  result: "error";
  code: number;
  reason: string;
};

type ApiResponse = ApiSuccess | ApiError;

result をタグにすることで、成功時は body を、失敗時は codereason を安全に参照できます。

【付録】さらに学びを深めるためのリソース


さらにTypescriptの学習を進めたい方のために、いくつかのリソースを紹介します。
これらのリソースを活用することで、TypeScriptの型システムについてより深い知識を得ることができるでしょう。

おすすめの書籍

ゼロからわかる TypeScript入門


技術評論社から出版されている「ゼロからわかる TypeScript入門」は、プログラミング初心者や本職プログラマーではない方を主な対象にした入門書です。

変数・条件分岐・ループといった基本から、クラスやインターフェース、モジュールまで段階的に学べる構成になっています。最終章ではWeb APIとJSONを使った非同期Webアプリの作成も体験できるので、「実際に動くものを作る」ところまで到達できます。

プロを目指す人のためのTypeScript入門


技術評論社の「プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで」、通称 ブルーベリー本 です。
JavaScriptの仕様とTypeScript独自の機能を両方押さえつつ、リテラル型・ユニオン型・keyof型・ジェネリクスなど、高度な型表現まで踏み込んで解説しています。TypeScriptの型システムの表現力を本格的に学べる一冊です。

オンラインで参照できる公式ドキュメント

TypeScript公式ハンドブック


https://www.typescriptlang.org/docs/
TypeScriptの公式ドキュメントです。
intersection型を含む、すべての型システムの機能について詳細な説明があります。

TypeScript Deep Dive


https://basarat.gitbook.io/typescript/
TypeScriptの深い部分まで掘り下げて解説しているオンラインブックです。
無料で読むことができ、intersection型についても詳しく説明されています。

TypeScriptの学習は終わりがありません。
新しい機能が常に追加され、より良い書き方が発見されています。
継続的に学習を続けることで、より良いTypeScriptプログラマーになれるはずです。


まとめ — Discriminated Unionは「タグで型を整理する」パターン

Discriminated Union のポイントをまとめます。

  • Discriminated Union とは、共通のリテラル型プロパティ(タグ)を持たせたユニオン型のパターン
  • タグの役割は「このオブジェクトがどの型か」をTypeScriptに伝えること
  • switch文と組み合わせることで、各ケースの中の型が自動的に絞り込まれる
  • never型と組み合わせると、型を追加したときの処理漏れをコンパイル時に検知できる
  • 使いどころは「状態の種類に応じてデータ構造が変わる」場面(ローディング状態、APIレスポンス分類など)

複数の状態や種類を型で表現したいときは、まずDiscriminated Unionのパターンが使えないか考えてみてください。コードの意図が伝わりやすくなり、型の追加・変更にも強くなります。

なかむぅ
なかむぅ
ユニオン型の基本や型ガードの仕組みからおさらいしたい方は、こちらの記事も参考にしてみてください。
TypeScriptのユニオン(Union)型とは?型ガードで「どの型か」を安全に絞り込む方法
TypeScriptのユニオン(Union)型とは?型ガードで「どの型か」を安全に絞り込む方法TypeScriptのユニオン型とは何かを基礎から解説。typeof・in・instanceofを使った型ガードの違いと使い分けも初心者向けにわかりやすく整理します。...
なかむぅ
なかむぅ
never型を使った網羅チェックについてさらに深掘りしたい方は、こちらの記事をご覧ください。Discriminated Union と never型を組み合わせることで、型の追加漏れをコンパイラに検知させるテクニックを詳しく解説しています。
TypeScriptのnever型 — 3つの登場シーンと使い所をわかりやすく解説TypeScriptを使っていると、ふとしたタイミングでコードの中に never という型が出てくることがあります。 ...

※本記事の本文案はAIを活用して作成していますが、記載している内容およびコードは筆者が実際に調査、検証・実行し、内容の正確性を確認した上で公開しています。