現場の型安全を極限まで高める:`Extract
こんにちは。日々、複雑怪奇なレガシーコードと最新のフロントエンド仕様の狭間で、TypeScriptの型システムをいじくり回しているチーフアーキテクトだ。
お前たちが日々書いているそのコード、本当に「型安全」と言えるか?
「とりあえず `any` を貼っておこう」「`as` で型アサーションして黙らせよう」——そんな妥協の産物が、本番環境で予期せぬランタイムエラーを引き起こし、深夜のPagerDutyを鳴らす原因になっていることに、そろそろ気づいている頃だろう。
大規模なWebアプリケーションを構築する上で、最も厄介な敵の一つが「ユニオン型の爆発」だ。APIのレスポンス、コンポーネントの状態遷移、Redux/Zustandのアクション、あらゆる場所でユニオン型が交差し、コンパイラが悲鳴を上げる。
今回は、そんなカオスを優雅に、そして厳格に制御するための奥義、`Extract
—
1. `Extract` の本質と条件付型の内部メカニズム
まずは基本の復習だが、ただの復習では終わらせない。
`Extract
その実装は、TypeScriptの標準ライブラリ(`lib.es5.d.ts`)内ではこのように定義されている。
type Extract
一見、拍子抜けするほどシンプルだ。だが、ここにTypeScriptの型システムの心臓部である 「分配条件付型(Distributive Conditional Types)」 の魔術が隠されている。
分配のカラクリを知る
ユニオン型を条件付型に渡した瞬間、TypeScriptのコンパイラは、そのユニオンのメンバーを一つずつバラバラにして、それぞれに対して条件判定を行う。これを「分配」と呼ぶ。
例えば、次のようなコードがあったとする。
type Primitive = string | number | boolean;
type OnlyStringOrNumber = Extract
// 結果: string | number
裏で何が起きているか? コンパイラはこう思考している。
1. `string extends (string | number) ? string : never` ➔ `string`
2. `number extends (string | number) ? number : never` ➔ `number`
3. `boolean extends (string | number) ? boolean : never` ➔ `never`
最後にこれらがユニオンで結合される。`string | number | never` だ。TypeScriptの型システムにおいて、`never` はユニオンの単位元(足し算における `0` や集合論における空集合)であるため、自動的に消去され、結果として `string | number` が残る。
この「不要なメンバーを `never` に落として消し去る」という挙動こそが、型安全なフィルタリングの根幹なのだ。
—
2. なぜ `Extract` が実務のアーキテクチャで神格化されるのか
「単なる型の引き算なら `Exclude
ここでは、実務で遭遇する具体的なユースケースを見ていこう。
ユースケース A: 複雑なイベント駆動アーキテクチャにおけるペイロードの絞り込み
フロントエンドでありがちなのが、多様なイベントをハンドリングするDispatcherの設計だ。
// アプリケーション全体で飛び交うイベントの総称(巨大なユニオン型)
type AppEvent =
| { type: ‘USER_LOGIN’; payload: { userId: string; token: string } }
| { type: ‘USER_LOGOUT’ }
| { type: ‘UPDATE_PROFILE’; payload: { username: string; email: string } }
| { type: ‘NOTIFICATION_RECEIVED’; payload: { message: string; urgent: boolean } };
// 特定のイベント型だけを抽出したい!
type UserEvents = Extract
// 結果:
// | { type: ‘USER_LOGIN’; payload: { userId: string; token: string } }
// | { type: ‘USER_LOGOUT’ }
テンプレートリテラル型と `Extract` を組み合わせることで、特定のプレフィックスを持つイベント群を一網打尽に抽出できる。
これにより、特定のドメインロジック(例:認証周り)を扱うモジュールにおいて、不要なイベントの型ノイズを完全に排除した状態で関数を実装できるようになる。
// 認証系イベントのみを受け入れる厳格なハンドラー
function handleAuthEvent(event: UserEvents) {
switch (event.type) {
case ‘USER_LOGIN’:
// TypeScriptはここで event が payload を持つことを完璧に推論する
console.log(event.payload.token);
break;
case ‘USER_LOGOUT’:
console.log(‘Logged out’);
break;
}
}
もしここで `Extract` を使わず、巨大な `AppEvent` をそのまま関数に受け渡していたらどうなるか? 担当外のイベントが混入する余地が生まれ、Switch文の網羅性チェック(`never` によるExhaustiveness Check)で余計な分岐を書く羽目になる。
—
3. パフォーマンスとコンパイルタイムの罠:巨大なユニオンと向き合う
さて、ここからがチーフアーキテクトとしての本領発揮だ。
TypeScriptの型チェッカー(TSServer)は、JITコンパイルやAST(抽象構文木)の解析において、メモリとCPUを大量に消費する。特に巨大なユニオン型に対する条件付型の適用は、コンパイル時間を劇的に悪化させる要因になる。
型の「深さ」と「幅」が引き起こすパフォーマンス劣化
もし `T` のユニオンの要素数が数千を超えるような、自動生成された巨大なAPIスキーマ(OpenAPIの型定義など)に対して無闇に `Extract` を多用すると、TypeScriptの型推論エンジンは無限ループに近い状態に陥り、IDEのレスポンスが数秒単位でフリーズするようになる。
アーキテクチャ上の対策:
1. ユニオンの事前縮小(Pruning): 不必要なプロパティを持つ型をあらかじめ除外してから `Extract` を適用する。
2. ヘルパー型のキャッシュ: 複雑な抽出ロジックは、インラインで書かずに一度名前付きの型(Type Alias)として定義し、コンパイラの型評価キャッシュを効かせる。
// 悪い例: インラインで複雑な抽出を繰り返す
function process1(data: Extract | Extract) { … }
// 良い例: 一度エイリアスとしてキャッシュし、コンパイラの負担を軽減する
type ExtractedAB = Extract;
type ExtractedAC = Extract;
function process1(data: ExtractedAB | ExtractedAC) { … }
このわずかな意識の差が、CI/CDパイプラインのビルド時間を何分も短縮し、開発者のメンタルヘルスを守るのだ。
—
4. 高度な応用:タグ付きユニオン(Discriminated Unions)との融合
TypeScriptの真骨頂といえば、タグ付きユニオンによる網羅性チェックだ。
`Extract` を極めると、既存のユニオン型から「特定の状態(State)」のサブセットをいとも簡単に切り出し、UIコンポーネントのPropsを完全に同期させることができる。
非同期データのフェッチ状態を表す、お馴染みのロバストな型定義を考えてみよう。
type AsyncState
| { status: ‘IDLE’; data: null; error: null }
| { status: ‘LOADING’; data: T | null; error: null }
| { status: ‘SUCCESS’; data: T; error: null }
| { status: ‘ERROR’; data: null; error: Error };
// 「完了またはエラー」の状態だけを抽出した型を作る
type FinishedState
この `FinishedState
// ローディング中やアイドル状態をコンパイルレベルで排除した表示コンポーネント
interface ResultViewProps
state: FinishedState
}
export const ResultView =
if (state.status === ‘ERROR’) {
return
;
}
// ここでは state.data は null ではないことが保証されている
return
;
};
もし親コンポーネントが `LOADING` 状態のままこの `ResultView` に値を渡そうものなら、TypeScriptのコンパイラが即座にエラーを吐き捨てる。
「おい、`LOADING` は `FinishedState` に含まれてねぇぞ」とな。この安心感こそが、我々がTypeScriptを使う理由だ。
—
5. まとめ:型は「ドキュメント」ではなく「実行不可能な仕様書」である
TypeScriptの型システムは、単なるコード補完の道具ではない。
それは、アプリケーションのビジネスロジック、データフロー、そして制約を表現する「実行不可能な、しかし絶対に嘘をつかない仕様書」である。
今回解説した `Extract
- 巨大なユニオン型に怯えるな。分配条件付型の挙動を理解し、コンパイル負荷に配慮せよ。
- ドメインの関心事ごとに型を切り出し、コンポーネントの責務を型レベルで強制せよ。
型定義に妥協した瞬間から、アプリケーションの腐敗が始まる。
お前たちのコードベースからバグの芽を摘み取るのは、他の誰でもない、お前自身の厳格な型設計なのだから。
さあ、エディタに戻って、その緩んだ型定義を `Extract` でキリッと引き締め直してこい。

コメント