TypeScriptで判別可能ユニオンを活用する


## 1. 判別可能ユニオンとは？

**判別可能ユニオン** とは、複数の異なる構造を持つ型をまとめて扱うためのパターンで、それぞれの型に**共通の「判別子」プロパティ**を持たせることで、**どの型かを一意に識別できるようにしたもの**です。

この設計により、**条件分岐による安全な型の絞り込み（ナローイング)** が可能となり、型安全なコードを簡潔に記述できます。


### 例：通知メッセージを扱う型

```ts
type InfoNotification = {
  type: "info";
  message: string;
};

type WarningNotification = {
  type: "warning";
  message: string;
  retryAfterSeconds: number;
};

type ErrorNotification = {
  type: "error";
  message: string;
  errorCode: number;
  supportUrl?: string;
};

type Notification = InfoNotification | WarningNotification | ErrorNotification;
```

このコードでは、3種類の通知（info・warning・error）を扱うために、それぞれ異なる**構造の型**を定義しています。

- `InfoNotification`：情報表示用の通知
- `WarningNotification`：注意喚起用で、再試行までの時間を含む
- `ErrorNotification`：エラー用で、エラーコードやサポートURLを持つ

それぞれに共通しているのは `type` プロパティです。この `type` が「判別子（discriminator）」となり、どの型なのかを判別するために使われます。

---

###  `Notification` 型の意味

```ts
type Notification = InfoNotification | WarningNotification | ErrorNotification;
```

これは **ユニオン型**です。つまり、`Notification` 型の変数には **3つのうちどれか1つ**の型の値を代入できます。

```ts
const info: Notification = {
  type: "info",
  message: "システムは正常に動作しています。"
};

const warning: Notification = {
  type: "warning",
  message: "サーバーが一時的に混雑しています。",
  retryAfterSeconds: 30,
};

const error: Notification = {
  type: "error",
  message: "データの取得に失敗しました。",
  errorCode: 500,
  supportUrl: "https://support.example.com",
};
```

###  なぜ `type` を使うのか？

もし `type` がなければ、どの構造に該当するのかを判別するのが困難になります。  
例えばこんな関数を考えてみてください。

```ts
function handle(notification: Notification) {
  if ("retryAfterSeconds" in notification) {
    // warning型かも？
  } else if ("errorCode" in notification) {
    // error型？
  }
}
```

↑こうすると、プロパティの存在を使って判別しなければならず、**分かりにくくて安全性も低い**です。

一方で `type` を判別子として使うと、次のように明示的かつ安全に型を判別できます：

```ts
function handle(notification: Notification) {
  if (notification.type === "info") {
    // notification は InfoNotification 型として扱われる
    console.log(notification.message);
  } else if (notification.type === "warning") {
    // WarningNotification 型にナローイングされる
    console.log(`⚠️ ${notification.message}（再試行まで ${notification.retryAfterSeconds} 秒）`);
  } else if (notification.type === "error") {
    // ErrorNotification 型にナローイングされる
    console.log(`❌ エラー: ${notification.message}（コード: ${notification.errorCode}）`);
  }
}
```

TypeScript は `type` の値を見て、自動的にその型に「絞り込み（ナローイング）」してくれるのです。



## 2. switch文による網羅性チェック

ユニオン型に対して `switch` 文で条件分岐を行う際に、「すべての可能な型（ケース）を漏れなく処理しているかどうか」を**コンパイル時にチェックする**仕組みのことです。

これにより、将来ユニオン型に新しい型が追加されたときに、分岐漏れを防げます。


### 例：判別可能ユニオンを `switch` で分岐

```ts
type Notification =
  | { type: "info"; message: string }
  | { type: "warning"; message: string; retryAfterSeconds: number }
  | { type: "error"; message: string; errorCode: number };

function handleNotification(notification: Notification) {
  switch (notification.type) {
    case "info":
      console.log(notification.message);
      break;
    case "warning":
      console.log(`${notification.message}（再試行まで ${notification.retryAfterSeconds} 秒）`);
      break;
    case "error":
      console.log(`${notification.message}（エラーコード: ${notification.errorCode}）`);
      break;
  }
}
```

このコードは今の時点では問題ありません。ただし、もし後から `type: "success"` のような通知タイプが追加された場合、`switch` 文はそれに対応しておらず、バグの温床になります。



### 解決策：`never` 型を使った網羅性チェック

```ts
function handleNotification(notification: Notification) {
  switch (notification.type) {
    case "info":
      console.log(notification.message);
      break;
    case "warning":
      console.log(`${notification.message}（再試行まで ${notification.retryAfterSeconds} 秒）`);
      break;
    case "error":
      console.log(`${notification.message}（エラーコード: ${notification.errorCode}）`);
      break;
    default:
      // このコードが保証するのは「すべてのcaseが処理されているか」
      const _: never = notification;
      throw new Error("未処理の通知タイプです");
  }
}
```

ここでの `const _: never = notification;` の意図は次の通りです：

- `notification` が `"info" | "warning" | "error"` のいずれかであれば、すでに `case` で処理済みなので `never` 型になっているはず
- もしユニオン型に新しい `"success"` などの値が追加されて `case` が足りていなければ、`notification` の型は `never` ではなくなる
- その結果、`never` 型への代入に失敗し、**TypeScriptがコンパイルエラーを出してくれる**

この仕組みにより、以下のようなコードの安全性が高まります：

- 型の追加漏れを**事前に防止**できる
- レビューやテストでの**人的ミスを軽減**できる
- 将来の仕様変更にも**強い構造**になる


### 補足：`if/else` では網羅性チェックできない理由

`switch` 文では `default` + `never` を使うことで網羅性チェックが可能ですが、`if/else if` の分岐構造では、TypeScript はすべての可能性を静的にチェックすることができません。

```ts
if (notification.type === "info") {
  // ...
} else if (notification.type === "warning") {
  // ...
} // "error" を忘れてもコンパイルエラーにならない
```

そのため、ユニオン型の全パターンを確実に処理する必要がある場面では、`switch` 文を使うのが望ましいです。



## 3. 判別可能ユニオンが活きる現実的なユースケース

### APIレスポンスの表現

Webアプリケーションでは、バックエンドAPIから返されるレスポンスが**成功時と失敗時で異なる構造**を持つことがよくあります。

#### 例：ログインAPIのレスポンス

```ts
type LoginSuccess = {
  type: "success";
  token: string;
  userId: string;
};

type LoginFailure = {
  type: "error";
  message: string;
};

type LoginResponse = LoginSuccess | LoginFailure;
```

このようにすることで、フロントエンドでは以下のような安全な処理が可能になります。

```ts
function handleLogin(response: LoginResponse) {
  if (response.type === "success") {
    // 成功時の型に絞られているため、補完が効き安全に扱える
    localStorage.setItem("token", response.token);
  } else if (response.type === "error") {
    alert(`ログイン失敗: ${response.message}`);
  }
}
```

- 型の絞り込みが可能 → エラー発生時の処理漏れを防げる  
- `switch` と組み合わせて網羅性チェックも可能  
- フロントエンドとバックエンドの契約（schema）として明確になる


### フォーム入力の型定義

1つのフォームで複数の入力モード（たとえば「住所入力」「連絡先入力」など）を切り替える場合、それぞれに異なる項目があります。

#### 例：ユーザープロフィール編集フォーム

```ts
type AddressForm = {
  type: "address";
  postalCode: string;
  prefecture: string;
  city: string;
};

type ContactForm = {
  type: "contact";
  email: string;
  phone: string;
};

type ProfileForm = AddressForm | ContactForm;
```

`type` を基準に表示・検証する項目を切り替えることで、**UIの構築とバリデーションが明確に分岐可能**になります。

```ts
function renderForm(form: ProfileForm) {
  if (form.type === "address") {
    // 住所入力フィールドを表示
  } else if (form.type === "contact") {
    // メール・電話番号入力フィールドを表示
  }
}
```

- フォームの入力モードごとのUIやバリデーションの定義を型で制御できる  
- `type` に応じた明示的な切り替えが可能になり、バグを防げる  
- 実際のユーザー操作と密接に結びつく処理で、保守性が高くなる


### 状態管理

アプリの状態（たとえば通信の状態や表示内容）を1つのユニオン型で管理することで、**状態ごとに異なる描画や処理を明確に分岐できる**ようになります。

#### 例：データのロード状態

```ts
type LoadingState = {
  type: "loading";
};

type SuccessState<T> = {
  type: "success";
  data: T;
};

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

type FetchState<T> = LoadingState | SuccessState<T> | ErrorState;
```

これにより、データフェッチの各状態に応じた表示が明確になります。

```ts
function render(state: FetchState<User[]>) {
  switch (state.type) {
    case "loading":
      return <p>読み込み中です...</p>;
    case "success":
      return <UserList data={state.data} />;
    case "error":
      return <p>エラー: {state.message}</p>;
  }
}
```

- 表示・非表示・エラーなどの状態を型で完全に管理できる  
- 状態の追加（例：`"empty"`など）にも型で対応可能  
- `switch` + `never` によって分岐漏れを検知できる



## まとめ

### 1. 判別子（`type` など）で明示的に型を分ける
各型に共通する「判別子」プロパティを持たせることで、どの型であるかを一意に識別できます。これにより、TypeScriptの型推論によって自然にナローイングが行われ、安全にプロパティへアクセスできます。

### 2. `switch` 文と `never` を組み合わせることで網羅性を確保できる
ユニオン型に新しいケースが追加されたときに、既存のコードに分岐漏れがないかをコンパイル時にチェックできます。これにより、将来的な拡張にも強いコードが書けます。

### 3. 実用的なユースケースが多い
判別可能ユニオンは、以下のような実務でも頻繁に利用されます。

- APIレスポンスの型定義（成功・エラーなどのパターン分岐）
- フォーム入力の種類による構造の違い
- 通信状態（読み込み中・成功・失敗など）を状態管理で表現する

### 4. 型安全で意図の明確なコードを実現できる
プロパティの存在チェックや型アサーション（`as`）に頼らず、明示的に処理を分岐できるため、**安全で保守しやすく、読みやすいコード**になります。