Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Discriminated Unions in TypeScript: How They Differ from Plain Type Unions

2025年09月15日に「Dev.to」が公開したITニュース「Discriminated Unions in TypeScript: How They Differ from Plain Type Unions」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

TypeScriptのUnion型には、単純な状態を表す「プレーン型」と、各状態に固有のデータを持てる「識別可能なUnion型」がある。後者は特定のプロパティで状態を区別し、TypeScriptがその状態に応じたデータの存在を認識するため、Reactなどで型安全なコードが書け、複雑な状態管理に役立つ。

ITニュース解説

システム開発において、プログラムがどのような状態にあるかを明確に管理することは非常に重要だ。特にJavaScriptに静的型付けの機能を追加するTypeScriptを使用する場合、この状態を型としてどのように表現するかが、コードの安全性や保守性に大きな影響を与える。例えば、Reactなどのフレームワークを用いたフロントエンド開発では、ユーザーインターフェースを構成するコンポーネントが「データを読み込み中」「処理が成功した」「エラーが発生した」といった様々な状態を持つことがよくある。これらの多様な状態を正確に、かつ安全に扱うための型定義として、「プレーンな型ユニオン」と「判別可能な型ユニオン」という二つの異なるアプローチがある。これらは名前は似ているが、機能面では大きな違いがあり、この違いを理解することが、より堅牢で信頼性の高いアプリケーションを構築する上で不可欠となる。

まず、最も基本的な型定義の一つである「プレーンな型ユニオン」から説明する。これは、複数の型や特定のリテラル値を|(または)で結合し、変数がその中のいずれかの型や値を取りうることを示す方法だ。例えば、フォームの処理状況を表すために、次のようなStatus型を定義できる。 type Status = "loading" | "success" | "error"; このStatus型を持つ変数statusは、"loading""success""error"のいずれかの文字列リテラル値を取ることができる。この型を使って、現在の状態に基づいてメッセージをレンダリングする関数を考えてみよう。 function render(status: Status) { if (status === "success") { return "Form submitted!"; } return "Not submitted"; } この関数は、status"success"であれば「Form submitted!」と表示し、そうでなければ「Not submitted」と返す。しかし、このプレーンな型ユニオンには明確な限界がある。それは、それぞれの状態に固有の追加情報を紐づけられないという点だ。もし"success"の状態になったときに、具体的にどのようなデータが送信されたのかを表示したい場合、"success"という文字列リテラル自体にはdataというプロパティを追加する場所がないため、型安全な方法で関連情報を扱うことができない。この型は、単に状態を「旗」のように示すだけで、それぞれの状態が持つ具体的なデータ構造までは表現できないのだ。そのため、このような型は非常に単純なフラグ管理には適しているが、より複雑なデータを持つ状態管理には不向きだと言える。

このプレーンな型ユニオンの限界を克服するために開発されたのが、「判別可能な型ユニオン(Discriminated Unions)」だ。これは、複数のオブジェクト型を|で結合して定義されるユニオン型の一種で、それぞれのオブジェクト型が共通の「判別プロパティ」を持つことが特徴である。判別プロパティは通常、"status""type"といった名前で、特定の文字列リテラルなどの固有の値を持つ。この判別プロパティがあることで、TypeScriptはユニオンの中から現在の具体的なオブジェクトの型を正確に判断(型ナローイング)できるようになる。 例えば、フォームの状態をより詳細に表現するために、次のようなFormState型を定義できる。 type FormState = | { status: "loading" } | { status: "success"; data: string } | { status: "error"; message: string }; このFormState型は、三つの異なるオブジェクト型のユニオンで構成されている。それぞれのオブジェクトはstatusという共通のプロパティを持ち、その値は"loading""success""error"のいずれかだ。ここで注目すべきは、"success"の状態を表すオブジェクトにはdataプロパティが、"error"の状態を表すオブジェクトにはmessageプロパティが、それぞれ固有に追加されている点である。 このFormState型を使って、フォームの状態に応じたUIをレンダリングする関数を見てみよう。 function renderForm(state: FormState) { if (state.status === "loading") return <p>Loading...</p>; if (state.status === "success") return <p>Submitted: {state.data}</p>; if (state.status === "error") return <p>Error: {state.message}</p>; } ここでTypeScriptの強力な型推論機能が発揮される。if (state.status === "success")という条件分岐ブロックに入ると、TypeScriptはstateの型を自動的に{ status: "success"; data: string }に絞り込んでくれるのだ。これにより、開発者はこのブロック内でstate.dataというプロパティに安全にアクセスできることがコンパイラによって保証される。同様に、if (state.status === "error")のブロック内ではstate.messageにアクセスできる。このように、共通の判別プロパティ(ここではstatus)の値に基づいて、TypeScriptがユニオン内の個々のオブジェクトの具体的な型を判別し、その型固有のプロパティにアクセスできることを保証する仕組みが、判別可能な型ユニオンの核心である。これにより、コードの安全性は大幅に向上し、実行時に存在しないプロパティにアクセスしようとしてエラーになるというような潜在的なバグを防ぐことができる。

判別可能な型ユニオンを使わない場合と使う場合では、コードの堅牢性において大きな違いが生じる。判別可能な型ユニオンを使わない場合、例えばtype FormState = "loading" | "success" | "error";のような型定義では、function renderForm(state: FormState)の中でif (state === "success")と条件分岐しても、stateの型は依然としてただの文字列リテラル型であり、dataのような追加のプロパティは存在しないため、アクセスできない。開発者は追加の情報を別々に管理するか、型アサーションを使って無理やりアクセスするしかなく、型安全性が損なわれる。 一方、判別可能な型ユニオンを使用する場合、type FormState = | { status: "loading" } | { status: "success"; data: string } | { status: "error"; message: string };という型定義を用いると、function renderForm(state: FormState)の中でif (state.status === "success")と条件分岐すると、TypeScriptはstateの型を{ status: "success"; data: string }と厳密に認識し、state.dataプロパティに安全にアクセスできることが保証される。これにより、開発者は「フォームが成功した際には、その成功データも一緒に表示する」といった、状態に紐づく具体的な情報を型安全に扱えるようになるのだ。これは単にコードが書きやすくなるだけでなく、コンパイラによる厳密なチェックが入ることで、潜在的なバグを開発段階で発見しやすくなるという大きなメリットがある。

判別可能な型ユニオンは、特にReactなどのコンポーネントベースのアプリケーション開発において、非常に強力な活用例がある。例えば、複雑なフォームの状態管理を考えてみよう。 type FormState = | { status: "idle" } | { status: "submitting" } | { status: "success"; result: string } | { status: "error"; message: string }; このようなFormStateをコンポーネントのプロパティとして受け取る場合、switch (state.status)文を使うことで、各状態に応じたUIを型安全にレンダリングできる。 function Form({ state }: { state: FormState }) { switch (state.status) { case "idle": return <button>Submit</button>; case "submitting": return <p>Submitting...</p>; case "success": return <p>{state.result}</p>; case "error": return <p>{state.message}</p>; } } このコードでは、"success"の状態であればstate.resultに、"error"の状態であればstate.messageに、TypeScriptの保証のもとで安全にアクセスできるため、コードが非常に堅牢になる。 別の活用例として、モーダルの表示状態を管理する場合を考えてみよう。 type ModalState = | { open: true; content: string } | { open: false }; このModalStateを受け取るモーダルコンポーネントでは、 function Modal({ state }: { state: ModalState }) { if (!state.open) return null; return <div>{state.content}</div>; } と記述することで、state.opentrueの場合にのみstate.contentが存在することをTypeScriptが保証してくれる。これにより、openfalseの時に誤ってcontentにアクセスしようとしてエラーになるような事態を防ぐことができる。このように、判別可能な型ユニオンは、状態に応じて異なるデータ構造を扱う際に、明確な型定義と強力な型推論を提供し、開発体験とコードの信頼性を向上させる。

結論として、プレーンな型ユニオンは、単純なフラグのような状態を示すには十分だが、それぞれの状態に固有のデータを持たせたい場合には、その機能は力不足である。一方、判別可能な型ユニオンは、共通の判別プロパティを持つオブジェクトのユニオンとして機能し、TypeScriptに個々のユニオンメンバーの具体的な型を正確に判別させ、その型固有のプロパティに安全にアクセスできることを保証する。特にReactのようなコンポーネントベースのアプリケーション開発において、コンポーネントの状態管理、APIレスポンスの処理、互いに排他的なプロパティの定義など、多様な状態とそれに付随するデータを扱う際には、判別可能な型ユニオンを積極的に採用することが強く推奨される。これにより、開発者は型安全で保守性の高い、そしてより堅牢なアプリケーションを構築できるようになる。

関連コンテンツ