【TypeScript】Discriminated Unionとは?タグで型を絞り込む仕組みを解説
ユニオン型を使い始めると、いつかこの壁にぶつかります。「オブジェクト型が増えてきたら、どうやって絞り込めばいいんだろう?」
この記事では、Discriminated Union(タグ付きユニオン)とは何か、なぜ必要なのか、どう書くのかを順番に解説します。「型に目印(タグ)を付けることで、型を安全に絞り込める」というパターンが核心です。
この記事は次のような方におすすめです。
- TypeScriptのユニオン型と型ガードの基本は理解している
- 実際のコードでDiscriminated Unionが出てきて意味がわからない
- 複数のオブジェクト型を扱うとき、型の絞り込みが煩雑になってきた
- switch文と型の絞り込みを組み合わせた書き方を知りたい
この記事を読むと、Discriminated Unionの概念・書き方・使いどころを一通り理解でき、「型で状態を表現する」という考え方が身につきます。
それでは、順を追って詳しく見ていきましょう!
- 未経験で後悔したくない
【実体験】未経験からITエンジニアに転職して後悔した話|4社経験してわかった「最初の選択ミス」 - 年収が低くて不安
4年間ずっと年収260万だったエンジニアが、転職で510万になるまでの全記録
通常のユニオン型で困る場面 — 絞り込みが複雑になるとき
ユニオン型はとても便利な機能ですが、複数のオブジェクト型を扱うときに少し悩ましいことが起きます。
複数のオブジェクト型が混在するユニオンは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 です。
Discriminated Unionとは — 「タグ」で型を識別するパターン
Discriminated Unionは、各オブジェクト型に「自分がどの型か」を示す共通のプロパティ(タグ)を持たせる設計パターンです。
「discriminated」は「区別された」という意味で、「union」はユニオン型のことです。直訳すると「区別可能なユニオン型」で、日本語では「判別可能なユニオン型」や「タグ付きユニオン」と呼ばれることもあります。
共通のリテラル型プロパティがポイント
Discriminated Union の核心は、すべての型に同じ名前のプロパティを持たせ、その値をリテラル型にすることです。
このプロパティのことを「discriminant(ディスクリミナント)」や「タグ」と呼びます。タグの値はそれぞれの型で異なるリテラル値にします。
よく使われるプロパティ名は kind や type ですが、名前はなんでもかまいません。
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" を追加し忘れると、default の 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 を、失敗時は code と reason を安全に参照できます。
【付録】さらに学びを深めるためのリソース
さらに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のパターンが使えないか考えてみてください。コードの意図が伝わりやすくなり、型の追加・変更にも強くなります。
※本記事の本文案はAIを活用して作成していますが、記載している内容およびコードは筆者が実際に調査、検証・実行し、内容の正確性を確認した上で公開しています。





