【実務・中級編】 Extract – TypeScript実践ガイド

こんにちは。チームのコードレビューをしていて、最近よく見かけるのが「なんとなく `any` を使ってごまかしているコード」や「巨大なユニオン型を前にして立ちすくんでいる姿」だ。気持ちは痛いほどわかる。型定義が複雑怪奇になってくると、コンパイラとケンカしているような気分になるからね。

だが、安心してほしい。TypeScriptの真価は、ユニオン型と条件エンティティ(Conditional Types)を組み合わせた瞬間に発揮される。今回は、その中でも実務で「おっ、こいつ分かってるな」と思わせる隠し味、`Extract` について徹底的に解説しよう。

公式ドキュメントの解説を読んでも「ユニオン型 `T` から `U` に割り当て可能な型を抽出する」としか書いてなくて、正直ピンとこないよな。今日は、実際のフロントエンド開発でどういう泥臭い課題をこいつで解決できるのか、裏側の動きも含めて叩き込んでいく。

—

1. `Extract` の正体と、その「裏側のメカニズム」

まず、言葉の定義を整理しよう。`Extract` は、TypeScriptが標準で用意しているユーティリティ型(Conditional Typesの具現化)の一つだ。

定義をTypeScriptのソースコード風に書くと、こうなっている。

type Extract = T extends U ? T : never;

……え?これだけ?と思うかもしれない。だが、この数行にTypeScriptの型パズルの真髄が詰まっている。
ここで重要なのは、ユニオン型に対してこれを適用したときの「分配法則(Distributive Conditional Types)」だ。

TypeScriptのコンパイラは、`T` がユニオン型である場合、そのユニオンの各要素を1つずつ取り出して、条件判定を個別に実行するというおせっかい(最高な機能)を裏側でやってくれている。

例えば、こういうことだ。

type Original = “a” | “b” | “c”;
type Target = “a” | “c” | “d”;

type Result = Extract;
// 結果: “a” | “c”

裏側で何が起きているか? コンパイラはこう思考している。
1. `”a”` は `Target` に含まれるか? → 含まれる(`”a”` を残す)
2. `”b”` は `Target` に含まれるか? → 含まれない(`never` にする)
3. `”c”` は `Target` に含まれるか? → 含まれる(`”c”` を残す)
4. 最終的に残った `”a”` と `”c”` をユニオンで結合する(`”a” | “c”`)

ユニオン型における `never` は「無(存在しないもの)」として扱われるため、綺麗に消え去る。これが `Extract` の魔法のタネだ。

—

2. 現場で即座に使える!実践的ユースケース

「理屈は分かったけど、実務のどこで使うんだよ」という君のために、俺たちが日々直面するコンポーネント設計のリアルな課題を解決してみせよう。

ユースケースA: デザインシステムのバリエーション絞り込み

例えば、共通ボタンコンポーネントを作っているとする。基本のサイズは全種類あるが、「特定のアイコン付きボタンでは、巨大な `xl` サイズはサポートしない」という要件があったとしよう。

ここで `Extract` を使うと、既存の型を壊さずに、安全にサブセットを作ることができる。

// 1. デザインシステム全体で定義されているすべてのボタンサイズ
type AllButtonSizes = “sm” | “md” | “lg” | “xl”;

// 2. アイコン付きボタンで「許可したい」サイズだけを定義
type AllowedIconSizes = “sm” | “md” | “lg”;

// 3. 【実践】全サイズから、アイコンボタンで使えるものだけを抽出する!
// 万が一、将来 “xs” が追加された時も、大元の型を修正すれば自動追従できる
type IconButtonSize = Extract;

// IconButtonSize の型は “sm” | “md” | “lg” になる(”xl” が綺麗に削ぎ落とされる)
const myIconButtonProps: { size: IconButtonSize } = {
size: “xl”, // ❌ ここでTypeScriptが「”xl” は割り当てられないよ」と怒ってくれる!安心!
};

どうだ? 「サイズが増減したときのメンテナンス性」が爆上がりするイメージが湧くだろうか。

—

ユースケースB: APIレスポンスやイベントの「特定ステータス」の絞り込み

フロントエンドでありがちなのが、非同期処理のステータスや、Redux / Zustand などの状態管理、あるいはWebSocketのイベント型をハンドリングする場面だ。

「特定のイベントタイプだけで処理を分岐させたい」というとき、`Extract` は最高の相棒になる。

// サーバーから送られてくる各種イベントの型(判別可能なunion型 / Discriminated Unions)
type ServerEvent =
| { type: “user_login”; userId: string; timestamp: number }
| { type: “user_logout”; userId: string }
| { type: “data_updated”; payload: Record };

// 【実践】”user” に関するイベントだけをゴッソリ抽出したい!
type UserEvents = Extract;

/
抽出された UserEvents の型:
| { type: “user_login”; userId: string; timestamp: number }
| { type: “user_logout”; userId: string }
/

// イベントをハンドリングする関数
function handleUserEvent(event: UserEvents) {
// event.type は “user_login” | “user_logout” に絞り込まれているため、
// “data_updated” の処理を間違えて書く心配がゼロになる
if (event.type === “user_login”) {
console.log(`ログインしたよ: ${event.userId}`);
}
}

テンプレートリテラル型(`user_${string}`)と `Extract` を組み合わせるこのテクニックは、大規模なフロントエンドアプリケーションのイベント駆動アーキテクチャで頻出する。ぜひコードベースに持ち帰って使ってみてほしい。

—

3. シニアから一言:よくあるアンチパターンと注意点

最後に、現場でたまに見かける「やってはいけない使い方」に軽く触れておこう。

1. プリミティブ型で何でもかんでも絞り込もうとしない
`Extract` のように、単に型を狭めたいだけなら、型ガード(`typeof`)や素の型注釈で十分なことが多い。`Extract` は「複雑なユニオン型から特定の条件で削ぎ落としたい時」に真価を発揮する。
2. `Exclude` との混同に注意
兄弟分である `Exclude` は「除外する(除く)」ものだ。

  • `Extract`:一致するものを残す
  • `Exclude`:一致するものを消す

この2つを混同すると、夜中の2時に頭を抱えることになるので、頭の片隅に置いておいてほしい。

—

まとめ

`Extract` は、単なるお便利ユーティリティではない。「型を人間の手で直書きするな、TypeScriptの推論能力に計算させろ」という、モダンフロントエンド開発の哲学を体現する強力な武器だ。

巨大な型定義に絶望したときは、「あ、ここは `Extract` で綺麗にスパッと切れるんじゃないか?」と思い出せるようになってほしい。君の書くコードが、より堅牢で、より美しいものになることを期待している。

それじゃあ、次のプルリクエストのレビューで会おう!

コメント

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