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

【ITニュース解説】Discriminated Unions in TypeScript explained simply

2025年09月20日に「Dev.to」が公開したITニュース「Discriminated Unions in TypeScript explained simply」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

TypeScriptの判別ユニオンは、アプリの状態を`kind`のような一つのプロパティで明確に定義する仕組みだ。これにより不可能な状態の発生を防ぎ、各状態に必要なデータのみを持たせることで型安全性が向上する。結果としてコードが分かりやすくなり、変更にも強くなる。

ITニュース解説

アプリケーション開発において、データの状態を管理することは非常に重要である。しかし、アプリケーションの状態をisLoadingdataerrorといった複数の独立したフラグで管理しようとすると、さまざまな問題が発生しがちである。例えば、isLoadingtrueなのに同時にerrorも設定されている、といった論理的に矛盾する状態が生まれてしまうことがある。このような状況では、現在のアプリケーションが実際にどのような状態にあるのかが不明確になり、コードの動作を予測したり、デバッグしたりすることが難しくなる。

このような問題を解決するために、TypeScriptで「判別可能な共用体(Discriminated Unions)」という強力な機能が利用できる。判別可能な共用体とは、「アプリケーションは一度に正確に一つの状態にあり、それぞれの状態はその状態にのみ必要なデータを持つ」という考え方に基づいた型定義の方法である。

具体的には、複数のオブジェクト型をパイプ記号|で結合した共用体型を定義し、その各オブジェクト型が共通のフィールド(例えばkindtypeといった名前のプロパティ)を持つようにする。この共通フィールドが「判別子(discriminant)」と呼ばれ、その値によって共用体の中から現在のアクティブな状態を判別する役割を果たす。

例えば、データの取得状態を管理する場合、次のような型を定義できる。 FetchState<T>という型は、データがない「idle(待機中)」、データを取得中の「loading(読み込み中)」、データ取得が成功した「success(成功)」、データ取得に失敗した「error(エラー)」のいずれかの状態を取る。 「idle」状態はデータを持たず、「loading」状態もデータを持たない。「success」状態は取得したデータ(data: T)のみを持ち、「error」状態はエラーメッセージ(message: string)のみを持つ。このようにすることで、loading状態とerror状態が同時に存在したり、success状態なのにdataがない、といった矛盾した状態を型レベルで排除できる。

この判別可能な共用体を利用することには、いくつかの大きな利点がある。まず、「明確性」が挙げられる。状態オブジェクトのkindプロパティを見れば、現在のアプリケーションがどのような状況にあるのかが一目でわかり、コードの意図が明確になる。次に「安全性」である。TypeScriptの型システムが、switch文などでkindプロパティの値に基づいて状態を判別すると、それぞれのcaseブロック内で、その状態に固有のプロパティが安全に利用できることを保証する。例えば、「success」の状態であれば、そのブロック内ではdataプロパティが必ず存在し、その型も正しく推論されるため、dataが存在するかどうかをチェックする手間が省け、コードがより堅牢になる。さらに「ガイダンス」も得られる。将来的に新しい状態を追加した場合、コンパイラがその新しい状態を処理していない箇所を自動的に検出し、開発者に更新が必要な場所を教えてくれるため、変更に強く、バグの発生を防ぐことに役立つ。

具体的な使用方法としては、まずアプリケーションの状態を表現する共用体型を定義する。この際、kindtypeといった単一のプロパティ名を判別子として選び、各状態オブジェクトにそのプロパティを持たせる。例えば、ユーザー認証の状態を表すAuthState型を定義するなら、loggedOut(ログアウト中)、loggingIn(ログイン処理中)、loggedIn(ログイン済み)、loginFailed(ログイン失敗)といった状態を定義する。loggedIn状態にはユーザー情報、loginFailed状態にはエラーメッセージといった、その状態固有のデータを含める。

次に、この定義した共用体型を使ってコードを記述する。最も一般的な方法は、判別子であるkindプロパティの値に基づいてswitch文を使用することである。switch文の各caseブロック内で、TypeScriptは自動的にそのブロック内での変数の型を、対応する個別の状態型に絞り込む。これにより、loggedIn状態であれば安全にユーザー名にアクセスでき、loginFailed状態であればエラーメッセージにアクセスできる。また、switch文のdefaultケースでassertNeverのようなヘルパー関数を呼び出すことで、すべての状態が処理されていることをコンパイラに強制させ、新しい状態が追加されたときに処理の漏れがないかをチェックできる。

状態を更新する際は、それぞれの状態変更を小さな純粋な関数として実装することが推奨される。純粋な関数とは、同じ入力に対して常に同じ出力を返し、外部の状態を変更しない関数のことである。例えば、ログイン開始、ログイン成功、ログイン失敗といった操作ごとに専用の関数を用意する。これらの関数は、現在の状態と新しい情報を受け取り、新しい状態オブジェクトを返すだけなので、テストが非常に容易になるというメリットがある。

判別可能な共用体は、さまざまな一般的なプログラミングパターンに応用できる。例えば、非同期通信でデータを取得する際にisLoadingフラグを使わずに、idleloadingsuccesserrorといった状態をFetchState<T>として表現できる。ユーザーインターフェースでこれらの状態に応じた表示を切り替える場合も、switch文を使えば明確かつ安全に実装できる。また、フォームの入力処理においても、editingsubmittingsubmittedfailedといったフォームのフローを状態として定義することで、isSubmittingsubmitErrorといった複数のフラグを不要にし、状態の意味をより明確にできる。さらに、機能フラグ(ロールアウト)の管理や、エラーハンドリングのためのResult<T, E>型(成功か失敗かを明示的に表現)や、値の有無を表すOption<T>型(値があるか、ないか)の実現にも利用できる。これらは、従来のtry-catchブロックやnullチェックを減らし、エラーや欠損値の扱いに安全で明示的な方法を提供する。

コードの可読性や利便性をさらに高めるための補助機能も存在する。例えば、特定の状態であるかどうかをチェックする「タイプガード」関数は、if文の中で状態を特定の型に絞り込み、プロパティへの安全なアクセスを可能にする。また、switch文の代わりに、より簡潔に状態ごとの処理を記述できる「マッチャー」関数も提供されることがあるが、これはあくまでオプションであり、標準的なswitch文でも十分に機能する。

既存のコードベースに判別可能な共用体を導入する際は、まず現在のコードで複数のフラグを使って状態を管理している箇所を特定する。次に、それらのフラグが表現しようとしている現実のアプリケーションの状態(例えばidle | loading | success | error)を定義し、各状態に固有のフィールドをその状態オブジェクト内に移動させる。そして、if文による状態チェックをswitch (state.kind)によるものに置き換え、assertNever関数を追加して網羅性を確保する。このような移行は、一度にすべてを行うのではなく、モジュール単位で少しずつ進めることで、すぐにその恩恵を実感できるだろう。

判別可能な共用体によって、状態変更を行う関数が純粋になるため、テストが非常に容易になるという大きなメリットがある。ネットワーク呼び出しやタイマーのモック化といった複雑な準備を必要とせず、入力と出力の関係だけをテストすればよいため、テストコードは短く、安定したものになる。

この強力なパターンを導入する上で注意すべき点もいくつかある。すべての状態オブジェクトで判別子のプロパティ名(例: kind)を統一すること、特定の状態でのみ必要なフィールドをオプションにせず、その状態オブジェクト内に含めること、意味やデータが同じであれば状態を統合すること、そしてassertNeverのような仕組みを使って網羅性を常にチェックし、新しい状態が追加されたときの処理漏れを防ぐことが重要である。

判別可能な共用体は、一見すると小さな機能に思えるかもしれないが、アプリケーションの状態に明確な名前を与え、論理的にあり得ない状態の発生を防ぎ、コードの可読性を向上させ、TypeScriptの強力な型チェックによって安全な変更を保証するという、非常に大きな価値を提供する。現在isLoadingerrordata?といった複数のフラグを使っている箇所から始めて、判別可能な共用体への移行を試してみることで、その違いをすぐに実感できるはずだ。

関連コンテンツ

関連IT用語

【ITニュース解説】Discriminated Unions in TypeScript explained simply | いっしー@Webエンジニア