まず「Discriminated Union」をざっくり日本語にすると
難しいカタカナですが、意味はシンプルです。
Discriminated Union(判別可能ユニオン)=「ある1つのプロパティの値によって、どの型なのかを“判別できる”Union型」 です。
もっとラフに言うと、
- 「状態を表す文字列」みたいな印をひとつ決めておいて
- その値ごとに「中身の型」を変えるやり方
です。
TypeScript 的には、
その「印になるプロパティ」(たとえば status や type、kind)を見て、
if や switch の中で自動的に型を絞り込んでくれる、とても相性のいい書き方です。
ここから、失敗しづらい形で、具体例で掘り下げていきます。
一番典型的な形:statusで状態を切り替える
状態を4パターン持つUnion型
たとえば「読み込み処理の状態」を型で表したいとします。
- まだ何もしていない(idle)
- 読み込み中(loading)
- 成功してデータを持っている(success)
- 失敗してエラーを持っている(error)
これを Discriminated Union で書くと、こうなります。
type LoadingState =
| {
status: "idle";
}
| {
status: "loading";
}
| {
status: "success";
data: string;
}
| {
status: "error";
error: string;
};
TypeScriptここで重要なのは、
statusが 全パターンに共通して存在するstatusの値が、それぞれ"idle" | "loading" | "success" | "error"のように「被らないリテラル文字列」- 他のプロパティ(
data,error)は「ある状態のときだけ存在する」
という構造です。
この「全パターンが同じ名前のプロパティ(ここでは status)を持ち、その値がパターンごとに違う」
という形が、Discriminated Union の核心です。
if / switch で分けたときに型が勝手に絞られる
この LoadingState を使うと、こんな関数が書けます。
function render(state: LoadingState) {
switch (state.status) {
case "idle":
console.log("待機中");
break;
case "loading":
console.log("読み込み中…");
break;
case "success":
console.log("成功:", state.data);
break;
case "error":
console.log("失敗:", state.error);
break;
}
}
TypeScriptここで起きている「型の世界」を言葉で追うと、こうなります。
case "idle"の中
→stateは{ status: "idle" }型に絞り込まれるcase "loading"の中
→{ status: "loading" }型に絞り込まれるcase "success"の中
→{ status: "success"; data: string }型に絞られるので、state.dataが安全に使えるcase "error"の中
→{ status: "error"; error: string }型に絞られるので、state.errorが安全に使える
つまり、status の値を見て分岐すると、その分岐内では「その状態に応じた型」に自動で狭まってくれる わけです。
これがまさに「判別可能(Discriminated)」な Union である理由です。
なぜ「判別用プロパティ」を1つ決めると嬉しいのか
「今どのバリエーションなのか」が一目でわかる
さっきの例だと、status という1つのプロパティを見れば、
そのオブジェクトが「今どの状態なのか」がすぐ分かります。
status: "idle"→ 何もしてないstatus: "loading"→ 読み込み中status: "success"→ 成功していて、dataがあるstatus: "error"→ 失敗していて、errorがある
コードを読む人間にとっても分かりやすいし、
TypeScript にとっても「ここは success のケースだから data があるよね」と理解しやすい形になっています。
「状態ごとにあるプロパティ」が自然に書ける
たとえば、「成功のときだけ data がある」ような設計は、
Discriminated Union で書くととても自然です。
type SuccessState = {
status: "success";
data: string;
};
TypeScriptこれだけで、
- success のときは必ず data がある
- success 以外のときは data がない
というルールが、型レベルで保証されます。
もしどこかで、
if (state.status === "success") {
console.log(state.error); // これはエラー
}
TypeScriptと書いたら、TypeScript が「success 状態には error はないよ」と怒ってくれます。
「状態によって何がある/ない」を、型としてキレイに表せる。
そして、そのルールから外れたコードを書こうとすると、ちゃんと止めてくれる。
ここが Discriminated Union の一番おいしいところです。
別の例:図形の種類をkindで判別する
Circle / Square / Rectangle などを1つの型にまとめる
図形を扱いたいときも、Discriminated Union がよく使われます。
type Circle = {
kind: "circle";
radius: number;
};
type Square = {
kind: "square";
size: number;
};
type Rectangle = {
kind: "rectangle";
width: number;
height: number;
};
type Shape = Circle | Square | Rectangle;
TypeScriptここでは kind が判別用プロパティです。kind の値を見れば、どの図形か一発で分かります。
kind で分岐して型を絞り込む
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
case "rectangle":
return shape.width * shape.height;
}
}
TypeScriptしれっと書いていますが、型の世界ではこうなっています。
case "circle"の中ではshapeはCircle型に絞られ、radiusが使えるcase "square"の中ではSquare型で、sizeが使えるcase "rectangle"の中ではRectangle型で、width,heightが使える
もし間違えて、たとえば case "square" の中で shape.radius と書いたら、
TypeScript に怒られます。
「square 型には radius はないよ」と。
「kind の値」と「図形ごとのプロパティ」がリンクしている からこそ、
TypeScript が強力にチェックしてくれる、というわけです。
よくある失敗パターンと、Discriminated Union での解決
悪い例:判別用プロパティを決めていないUnion
例えば、こんな型を考えます。
type BadShape =
| { radius: number }
| { size: number };
TypeScriptここには「判別用プロパティ」がありません。radius があるかどうかで見分けたくなりますが、
実務が複雑になるとこういう判定は増えすぎてぐちゃぐちゃになりがちです。
また、switch もしづらいです。
なぜなら「何を基準にケースを分けるのか」が曖昧だから。
これを Discriminated Union にすると、こうなります。
type GoodShape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number };
TypeScriptすると、分岐は必ず shape.kind ベースで書けるようになります。
function area(shape: GoodShape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
} else {
return shape.size ** 2;
}
}
TypeScript「1つの明確な“判別軸”を決める」ことで、分岐のロジックも型の絞り込みもきれいに整理される
これが Discriminated Union の強さです。
設計の視点:どうやって「判別用プロパティ」を選ぶか
その値だけ見れば「何パターンのうちどれか」が分かる軸を探す
Discriminated Union を設計するとき、一番考えるべきことはこれです。
「このドメイン(問題の世界)で、
このオブジェクトの“ケース”を分ける自然な軸は何か?」
状態なら status や state
種類なら type や kind
ロールなら role
などが典型的です。
たとえばチャットアプリで、
- テキストメッセージ
- 画像メッセージ
- システムメッセージ
みたいなものを扱うなら、こんな感じにできます。
type Message =
| { type: "text"; text: string }
| { type: "image"; url: string }
| { type: "system"; code: number; message: string };
TypeScriptここでも type が判別用プロパティです。
Message を扱う関数は、たいてい message.type を見て分岐します。
このとき、今どのパターンかが一瞬で分かり、
かつ TypeScript が型を絞り込んでくれるので、
実装と型と読みやすさが、きれいにそろいます。
判別用プロパティは「全ケース共通」「値が被らない」ことが重要
Discriminated Union をちゃんと機能させるには、次の2つが大事です。
全てのケースに、その判別用プロパティが必ず存在する
各ケースで、そのプロパティの値が“被らない”
例えば status なら、
idle / loading / success / error
のように、各状態で異なる文字列リテラルを持たせる必要があります。
もしどこかで値が被ってしまうと、
TypeScript からすると「status が ‘done’ のとき、この2つの型どちらか分からない」
という状態になり、絞り込みがうまく働かなくなってしまいます。
まとめ:Discriminated Unionを自分の言葉で言うと
自分の言葉で整理すると、Discriminated Union はこういうものです。
「状態や種類を表す“ラベル”用プロパティを1つ決めておいて、
その値ごとに中身の型を変える Union。
そのラベルを見れば、どの型なのかが一目で分かるようになっている。」
そして、
そのラベル(status や type)を if / switch で分岐すると、
TypeScript が自動的に「じゃあこの中ではこの型だね」と型を絞り込んでくれる。
だから、
状態ごとに違うプロパティを持つ
種類ごとに違う情報を抱える
といった「現実の“場合分け”」を、
きれいに、安全に、そのまま型で表現できる。
コードを書いていて、
「これはいくつかの“パターン”があって、そのパターンごとに持ってる情報が違うな」
と感じたら、そこが Discriminated Union を検討するタイミングです。
そのとき、
この世界では “何” を見ればパターンが分かる?
を自分に問いかけて、その答えを status, type, kind などの判別用プロパティとして形にしてみてください。
それが、そのまま TypeScript にとっての「判別の軸」になり、
あなたの頭の中の“場合分け”と、コンパイラの“型の理解”が、ぴったり重なり始めます。
