【実務・中級編】 Extractによるユニオン型の抽出 – TypeScript実践ガイド

こんにちは。チームのコードレビューをしていて、「お、ここ綺麗に型がつながってるね」と感心させられることもあれば、「うわっ、また `any` が生えてる……」と頭を抱えたくなる瞬間、フロントエンドの現場では日常茶飯事ですよね。

特に、APIから飛んできた雑多なレスポンスや、複雑に絡み合うUIの状態をTypeScriptでハンドリングするとき、ユニオン型(Union Types)の扱いで夜な夜な頭を悩ませている中級エンジニアの姿を目にします。

今回は、そんなユニオン型のジャングルを華麗に切り開くための秘密兵器、`Extract` についてお話しします。公式ドキュメントのサラッとした説明だけでは見えてこない、「現場でどう使えて、裏でTypeScriptのコンパイラがどう暴れているのか」という泥臭いところまで、しっかり解説していきますよ。

コーヒーでも飲みながら、ゆっくり読んでいってください。

—

1. `Extract` とは一体何者か?

まずは基本のおさらいです。`Extract` は、TypeScriptが標準で用意してくれているユーティリティ型のひとつです。定義をごくシンプルに書くとこうなります。

type Extract = T extends U ? T : never;

「……ん? 三項演算子?」と思ったそこのあなた、大正解です。これ、実は条件付き型(Conditional Types)と、ユニオン型の分散的条件型(Distributive Conditional Types)という仕組みを巧みに利用した、TypeScriptの真骨頂とも言えるコードなんです。

ざっくり日本語で機能を説明するなら、「ユニオン型 `T` の中から、型 `U` に割り当て可能な(compatibleな)ものだけを引っこ抜いて、新しいユニオン型を作る」というものです。

ブラウザ(tsc)の裏側で何が起きているのか?

「型定義なんて、どうせブラウザで動く頃には消えてるんでしょ?」
ええ、その通りです。TypeScriptの型はランタイム時には綺麗さっぱり消え去り、JavaScriptのコードだけがブラウザのエンジン(V8など)に放り込まれます。

しかし、コンパイル時(ビルド時)のTypeScriptコンパイラ(`tsc`)の頭の中では、この `Extract` が評価されるとき、ものすごい勢いで「型合わせのパズル」が行われています。

コンパイラは、ユニオン型 `T` を一度「バラバラのピース」に分解し、一つひとつに対して「お前は `U` に代入できるか?」と厳しく審査します。

  • 代入できたら(`T extends U` が真なら) → そのまま残す
  • 代入できなかったら(偽なら) → `never`(「そんな型は存在しない」という無の空間)に叩き落とす

最後に、生き残ったピースたちをもう一度ユニオン型として再結合する。これが、`Extract` の裏側の舞台裏です。コンパイラが汗をかいてやってくれているこの「仕分け作業」のおかげで、私たちのエディタには強力な補完が効くようになっています。

—

2. 現場で使える!実践コード例

理屈はこれくらいにして、実際のフロントエンド開発でどう使うのか、コピペしてそのまま動かせるサンプルを見てみましょう。

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

// サーバーとの通信で発生しうるすべてのステータス
type NetworkState =
| { status: “idle” }
| { status: “loading”; progress: number }
| { status: “success”; data: string }
| { status: “error”; error: Error };

// 【実務のユースケース 1】
// 「ローディング中」と「成功時」だけ、特定の共通処理で使いたい!
// そんなときは Extract でピンポイントに型を抽出します。
type ActiveOrFinishedState = Extract< NetworkState, { status: "loading" } | { status: "success" } >;

// 抽出された型の正体:
// { status: “loading”; progress: number } | { status: “success”; data: string }

// — 実際のコンポーネントや関数での利用イメージ —
function handleProgressOrData(state: ActiveOrFinishedState) {
// TypeScriptの賢いナローイングのおかげで、statusによる分岐が安全に行えます
if (state.status === “loading”) {
console.log(`読み込み中… 进捗: ${state.progress}%`);
} else {
// ここは自動的に { status: “success”; data: string } に絞り込まれる!
console.log(`成功! データ: ${state.data}`);
}
}

どうでしょう?
すべてのステータスが混ざった `NetworkState` から、自分が本当に扱いたい状態の型だけをスライスして新しい型を作れました。わざわざ似たような型をもう一度書き下す必要なんてありません。DRY原則は、ロジックだけでなく型定義にも適用すべきです。

—

3. 基本の型(primitive)でも大活躍する `Extract`

オブジェクトのユニオン型だけでなく、プリミティブな型がごちゃ混ぜになったユニオン型を整理する時にも、`Extract` はぶっちぎりの強さを発揮します。

例えば、APIから返ってくる謎の文字列の配列や、イベントの種別を絞り込むときです。

// カオスなイベント定義のユニオン型
type AllEvents = “click” | “focus” | “blur” | “scroll” | “resize” | 100 | 200;

// この中から、文字列(string)のイベントだけを抽出したい!
type StringEvents = Extract;
// 結果: “click” | “focus” | “blur” | “scroll” | “resize”

// 逆に、数値(number)だけが欲しいなら?
type NumericCodes = Extract;
// 結果: 100 | 200

「いや、そんなの最初から分けて定義しろよ」と思いましたか?
まったくその通りです。理想を言えば最初から綺麗に設計されているべきです。しかし、外部のサードパーティライブラリや、手が入らないレガシーなAPIスキーマから送られてくる型定義は、往々にしてカオスな状態です。

そんな「どうしようもない泥臭い現実」をスマートにハックし、私たちのフロントエンドのコードを安全に保つための防波堤として、`Extract` は最高の相棒になってくれます。

—

4. シニアから一言:注意点とベストプラクティス

最後に、現場で `Extract` を使うときの心構えと注意点をいくつか。

1. `Exclude` との使い分けをマスターする
`Extract` が「一致するものを抽出する」のに対し、対になる `Exclude` は「一致するものを除外する」動きをします。この2つを自由自在に操れるようになると、型パズルの引き出しが一気に広がります。
2. 型定義で複雑なことをしすぎない
`Extract` を何重にもネストさせたり、複雑な条件付き型を作りすぎると、コンパイル速度(IDEのレスポンス)が露骨に低下します。「あ、今俺、自己満足で複雑な型パズルやってるな」と気づいたら、一度立ち止まってインターフェースの設計自体を見直す勇気も持ちましょう。

TypeScriptの型システムは、ただエラーを防ぐための窮屈な鎖ではありません。「未来の自分やチームメンバーへの、最高に優しいラブレター」です。

`Extract` を正しく使いこなし、型安全でメンテナンス性の高い、美しいフロントエンドコードベースを一緒に築き上げていきましょう。それでは、また次回のコードレビューでお会いしましょう!

コメント

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