お疲れ。最近、君の書くコードを見ていて「おっ、いい感じに型安全を意識できてるじゃん」って感心してたところだ。でもな、そろそろ中級から一段上の「シニアの扉」を叩くタイミングだ。単に動くだけの型定義から、意図を雄弁に語るスマートな型設計へシフトしていこう。
今回は、実務で毎日といっていいほどお世話になる、だけど使い方を誤ると途端に魔改造の温床になるユーティリティ型 `Exclude
公式ドキュメントには「ユニオン型 `T` から `U` に含まれる型を除外する」ってサラッと書いてあるけど、あれだけじゃ実務の泥臭い課題は解決できない。今日で完全にモノにしてくれ。
—
1. `Exclude` の基本仕様と「条件付き型」の正体
まずはおさらいだ。`Exclude` の型定義を頭に思い浮かべてみてほしい。大体こうなってるはずだ。
type Exclude
見たことあるだろ? この `T extends U ? never : T` という書き方、これを TypeScript の世界では条件付き型(Conditional Types)と呼ぶ。
ここで一つ、現場でよくある勘違いを正しておこう。この `Exclude`、実は 分散条件付き型(Distributive Conditional Types) という性質を持っている。
ユニオン型が `T` に渡されたとき、TypeScript はそれをバラバラに分解して、一つひとつの要素に対して `extends U` の判定を裏側で泥臭く行っているんだ。
ブラウザ(tsc)の裏側の動きを覗き見する
例えば、こんな型があったとする。
type Colors = “red” | “green” | “blue” | “yellow”;
type PrimaryColors = Exclude
TypeScript のコンパイラ(`tsc`)が内部でどう処理しているかというと、ユニオンの各要素を次のように個別に評価している。
1. `”red” extends (“green” | “yellow”)` → 偽なので `”red”` を残す
2. `”green” extends (“green” | “yellow”)` → 真なので `never` に変える
3. `”blue” extends (“green” | “yellow”)` → 偽なので `”blue”` を残す
4. `”yellow” extends (“green” | “yellow”)` → 真なので `never` に変える
そして、残った結果たちをもう一度パイプ(`|`)で結び直す。TypeScript の世界において `never` は「無(存在しないもの)」としてユニオンから綺麗に消え去る仕様になっている。だから最終的に `”red” | “blue”` が手に入るというわけだ。見事なロジックだろ?
—
2. 現場で即コピペできる!実践的なユースケース
「理屈はわかったけど、実際のフロントエンド開発でどこに使うんだよ?」って思ったよな。
よくある3つの実務シーンをコードベースで解説しよう。エディタを開く準備はいいか?
ユースケース A: イベントハンドラーの絞り込み(UIコンポーネントの型安全化)
デザインシステムや共通UIを作っているとき、特定のイベントだけを排除したい要件によくぶつかる。
// 共通UIが受け取りうるすべてのイベント種別
type AllEventTypes = “click” | “focus” | “blur” | “keydown” | “scroll”;
// 特定のコンポーネント(例:スクロールを絶対にさせたくないモーダル)では “scroll” を除外したい
type ModalEventTypes = Exclude
// 実装時のハンドラー定義
const handleModalEvent = (eventType: ModalEventTypes) => {
console.log(`Handling event: ${eventType}`);
};
// OK
handleModalEvent(“click”);
// NG: 型エラーになる!「”scroll” は ModalEventTypes に割り当てられません」
// handleModalEvent(“scroll”);
手動でユニオン型を書き直すのは「型の一元管理」の原則に反する。元の `AllEventTypes` を変更すれば `ModalEventTypes` も自動追従する。この「変化に強いコード」がシニアの仕事だ。
ユースケース B: APIレスポンスのステータス管理
サーバーサイドから返ってくるステータスコードのうち、特定の例外を除外してフロント側で安全にハンドリングしたい時。
type ApiResponseStatus = 200 | 201 | 400 | 401 | 403 | 404 | 500;
// クライアント側で「成功系(200番台)」や「致命的エラー(500番台)」を扱う前に、
// まずは「認証系エラー(401, 403)」を共通インターセプターで弾いたあとの残りを取り回すケース
type BusinessLogicStatuses = Exclude
function handleApiResponse(status: BusinessLogicStatuses) {
switch (status) {
case 200:
case 201:
console.log(“成功!”);
break;
case 400:
case 404:
console.log(“リクエストエラー”);
break;
case 500:
console.log(“サーバーが爆発しました”);
break;
default:
// ここで万が一漏れがあったら TypeScript が教えてくれる(網羅性チェック)
const exhaustiveCheck: never = status;
throw new Error(`予期せぬステータスです: ${exhaustiveCheck}`);
}
}
ユースケース C: プリミティブ型や特定の型カテゴリの排除
これは少し高度なテクニックだ。`string` や `number` が混ざったジェネリックなユニオン型から、`null` や `undefined` のような「空っぽになりうる値」を排除したいときに使う。
type FormValue = string | number | boolean | null | undefined;
// 入力値として有効なもの(nullとundefinedを除外)だけを抽出したい
// 「何を除外するか」を Exclude で指定する
type ValidFormValue = Exclude
const processInput = (value: ValidFormValue) => {
// この中では value は絶対に null / undefined にならないことが保証される
if (typeof value === “string”) {
return value.trim();
}
return value;
};
—
3. 陥りがちな罠とシニアからのアドバイス
最後に、現場でよく見かける「やりがちミス」を共有しておく。これを知っておくだけで、無駄なデバッグ時間を何時間も節約できるはずだ。
罠 1: オブジェクト型そのものはうまく除外できないことがある
`Exclude` は基本的にユニオン型を対象に真価を発揮する。例えば、オブジェクトの構造そのものを除外しようとして、期待通りにいかなくてハマるジュニアが後を絶たない。
type User = { id: number; name: string };
type Admin = { id: number; name: string; role: “admin” };
// オブジェクト型をそのまま排除しようとしても、参照が違うと弾けない場合がある
type Result = Exclude
// ↑ これ、実は Admin は除外されない(構造の部分一致では分散条件付き型がうまく働かないことがある)
オブジェクトのプロパティで絞り込みたいときは、`Exclude` 単体ではなく、Omit や、識別子を持たせたユニオン(Discriminated Unions)を使うのが定石だ。型を適用する対象が「単一のプリミティブやリテラルのユニオン」なのか、「複雑なオブジェクト」なのかを常に意識してくれ。
—
まとめ
`Exclude
「変更に強い、拡張性のある型アーキテクチャを作るためのカッターナイフ」だ。
フロントエンドの規模が大きくなればなるほど、型の重複定義や、手動での修正はバグの温床になる。「元の型を正として、そこから引き算で安全なサブセットを作る」という発想を身につければ、君の書く TypeScript の品質は一段と洗練されるはずだ。
さて、理論はここまで。実際に自分のプロジェクトのコードを開いて、無駄に長く書かれたユニオン型を `Exclude` でスッキリ書き換えてみな。何か詰まったら、いつでも俺のところに相談にこいよ!

コメント