【実務・中級編】 Excludeによるユニオン型の絞り込み – TypeScript実践ガイド

やあ、調子はどうだい?
日々のコンポーネント実装やAPIの型定義に追われて、気がついたら `any` の爆弾を抱えて冷や汗をかく……なんていう泥臭いフロントエンドの戦場を、君も毎日のように駆け抜けていることだと思う。

さて、今日は中級から一歩抜け出して「本当の意味でTypeScriptを手懐ける」ための必須スキル、`Exclude` について話をしよう。

公式ドキュメントをサラッと読めば「ユニオン型 `T` から `U` に割り当てられる型を除外するユーティリティ型ね、はいはい」で終わりがちだ。だがな、実務の現場では、この `Exclude` こがUIの状態管理やAPIレスポンスの厳密な絞り込みにおいて、バグを未然に防ぐための強力な武器になるんだ。

今日はブラウザの裏側の話も含めて、このモヤモヤを完全にクリアにしてやろう。

—

そもそも `Exclude` って裏側でどう動いてるの?

まず大前提として、TypeScriptの型システムは「コンパイル時に消え去る幻」だ。ブラウザ(JavaScriptエンジン)が実行する時には、型なんてものは跡形もなく消えている。

じゃあ、TypeScriptのコンパイラ(tsc)はこの `Exclude` を見て裏で何をやっているのか?
答えは、「条件付き型(Conditional Types)」と「分散条件付き型(Distributive Conditional Types)」の組み合わせだ。

TypeScriptの内部では、以下のようなジェネリック型として定義されている。

type Exclude = T extends U ? never : T;

「もし `T` が `U` に割り当て可能なら `never` にし、そうでなければ `T` をそのまま返す」という非常にシンプルな仕組みだ。

ここにユニオン型を放り込むと、TypeScriptのコンパイラは「分散(Distribution)」というエコシステムを爆発させる。
例えば、`Exclude<'a' | 'b' | 'c', 'a'>` と書いたとき、コンパイラは内部で次のようなバラバラの判定を同時に行っている。

1. `’a’ extends ‘a’ ? never : ‘a’` → `never`(除外される)
2. `’b’ extends ‘a’ ? never : ‘b’` → `’b’`(残る)
3. `’c’ extends ‘a’ ? never : ‘c’` → `’c’`(残る)

結果として、`never | ‘b’ | ‘c’` となるわけだが、TypeScriptの型システムにおいて `never` は「無(何もない空間)」なので、ユニオンから自動的に消し去られる。だから `’b’ | ‘c’` だけが手元に残るという寸法だ。
このメカニズムを知っておくだけで、複雑な型エラーに直面したときも「今、コンパイラの中で何がどう分散しているのか」が頭に浮かぶようになる。これがシニアへの第一歩だ。

—

現場で即コピペできる!実践的なコード例

理屈はこれくらいにして、実際のフロントエンド開発でどう使うのか、泥臭くてリアルなユースケースを見ていこう。

今回は、よくある「非同期処理のステータス管理」を題材にする。

ユースケース:ローディング完了後の「エラーなし」状態の絞り込み

API通信の状態を表すユニオン型があったとする。ここで、「まだ始まっていない状態(`idle`)」と「ローディング中(`loading`)」を除外して、結果が確定した状態(成功か失敗)だけを扱うハンドラーを作りたい、そんな場面を想像してほしい。

以下のコードをそのままエディタに貼り付けて、型推論の動きを確認してみてくれ。

/

  • 1. アプリケーション全体で使うAPIの基本ステータス型

/
type ApiStatus = ‘idle’ | ‘loading’ | ‘success’ | ‘error’;

/

  • 2. Excludeを使って、不要な初期・ロード中ステータスを除外する
  • 結果として ‘success’ | ‘error’ のユニオン型が出来上がる

/
type FinishedApiStatus = Exclude;

/

  • 3. 実務でよくある、ステータスに応じた処理分岐の関数

/
function handleApiResponse(status: FinishedApiStatus, data: unknown) {
// ここに到達した時点で、status は ‘success’ か ‘error’ のどちらかに絞り込まれている
if (status === ‘success’) {
console.log(‘データ取得成功:’, data);
// 成功時の処理…
} else {
console.error(‘データ取得失敗’);
// エラー時の処理…
}
}

// — 動作確認用(エラーにならないケース) —
// ‘success’ は FinishedApiStatus に含まれるためOK
handleApiResponse(‘success’, { id: 1, name: ‘TypeScript Architect’ });

// ‘error’ も含まれるためOK
handleApiResponse(‘error’, null);

/
— コンパイルエラーになるケース —
もしここで ‘idle’ や ‘loading’ を渡そうとすると、TypeScriptのコンパイラが
「おいおい、その型は許容されてないぞ」と怒ってくれる。
/
// handleApiResponse(‘loading’, null); // ❌ 怒られる:Argument of type ‘”loading”‘ is not assignable to parameter of type ‘FinishedApiStatus’.

どうだ? 「特定のステータスだけを排除した新しい型」を定義し直すために、わざわざ `ApiStatus` とは別に新しいユニオン型をゼロから手書きする必要はない。`Exclude` を使えば、大元の型変更にも強くてメンテナンス性の高いコードが書ける。これが「DRY原則の型定義版」ってやつだ。

—

シニアから君へ送るベストプラクティスと注意点

最後に、実務で `Exclude` を扱う上での大切な心構えをいくつか伝えておこう。

1. `Omit` との使い分けを間違えるな!

  • ユニオン型(`string | number` や `’a’ | ‘b’`) から要素を削りたいときは `Exclude`。
  • オブジェクトのプロパティを削りたいときは `Omit`。

これ、中級によくある間違いだ。オブジェクト型に対してうっかり `Exclude` を使うと「あれ、型が `never` になっちゃったんだけど!?」と頭を抱えることになるので注意しろよ。

2. 型パズルに溺れるな、保守性を優先しろ

  • `Exclude` を何重にもネストさせたり、複雑な条件付き型と組み合わせすぎると、他のチームメンバー(あるいは3ヶ月後の自分)がコードを見たときに解読不能の呪文書と化す。
  • 型はあくまで「バグを防ぐためのガードレール」だ。複雑な絞り込みをするときは、一度途中経過の型に分かりやすい名前(Alias)をつけて、可読性を担保する配慮を忘れないでほしい。

さて、今日の講義はここまでだ。
明日から君の書くコンポーネントやカスタムフックの型定義の中に、早速この `Exclude` を取り入れてみてくれ。コードの美しさが一段階グッと引き締まるはずだ。

何かにつまずいたら、いつでもまた相談に来い。応援しているぞ!

コメント

タイトルとURLをコピーしました