はじめに
前回
条件付き型(Conditional Types)は、値の型に応じて別の型を返す 型レベルの if 文 である。本稿では以下を扱う。
- 条件付き型の基本構文
-
infer
による型抽出 - 実務で役立つユーティリティの自作
- パフォーマンスと可読性の最適化
1. 基本構文
type IsString<T> = T extends string ? true : false;
-
T extends U ? X : Y
という形で書き、T
がU
を満たすときX
を、そうでなければY
を返す。 -
分配法則:
T
がユニオンの場合、各メンバーに対して条件が評価される。
type R = IsString<"abc" | 123>; // true | false
2. infer
で型を抽出する
infer
は条件付き型の extends
節にだけ書ける特殊キーワードで、
型の一部を変数として捕捉する 機構である。
type Return<T> = T extends (...args: any) => infer R ? R : never;
function foo() { return { id: 1, name: "Taro" } as const; }
type FooRet = Return<typeof foo>; // { readonly id: 1; readonly name: "Taro" }
-
infer R
の位置に来た型がR
として推論される。 - 捕捉に失敗した場合(関数型でない場合)は
never
になる。
3. 実務ユーティリティの自作
3.1 Promise の中身を取り出す
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type Data = Awaited<Promise<Promise<number>>>; // number
3.2 配列の要素型を取得
type Elem<T> = T extends (infer U)[] ? U : T;
type Str = Elem<string[]>; // string
3.3 オブジェクトのキーを絞り込み
type KeysWithType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never
}[keyof T];
interface Mixed {
id: number;
name: string;
flag: boolean;
}
type StringKeys = KeysWithType<Mixed, string>; // "name"
4. パフォーマンス最適化
症状 | 原因 | 改善策 |
---|---|---|
ビルドが遅い | 再帰条件型が深い | 再帰を 2 段までに抑え、中間型をキャッシュする |
型エラーが読めない |
infer が多重ネスト |
抽出型に意味のある名前を付けて分割する |
分配が暴走 | 広いユニオンに条件型を直接当てる | 事前にキーを限定し、必要最小限だけ評価する |
5. よくある落とし穴
-
any
汚染: ジェネリックにany
が入ると条件式が緩み、型安全性が低下する。unknown
を使ってガードする。 -
推論の破綻:
infer
の位置が不適切だとnever
になりやすい。必要な型だけを抽出するよう構造を調整する。 - 再帰型の限界: 再帰深度は実質 45~50 で打ち止め。深い再帰はループ構造に置き換えるか、ユーティリティを 2 段構えに分割する。
まとめ
条件付き型と infer
は、TypeScript の型システムを 動的に組み替える 強力なツールである。
- 小さく作り、再利用する:巨大条件型は分割して可読性を確保。
-
infer
は最小限に:抽出したい部分だけを捕捉し、推論破綻を防ぐ。 - 評価コストを意識:分配・再帰はビルド時間とトレードオフになる。
次回は 型安全な API 通信:Zod と組み合わせる実践例 を扱い、型システムをランタイム安全性へつなげていく。