【TypeScript】条件型(Conditional Types)完全ガイド|T extends U ? X : Y・分配・infer・ユーティリティ型の実装まで徹底解説

【TypeScript】条件型(Conditional Types)完全ガイド|T extends U ? X : Y・分配・infer・ユーティリティ型の実装まで徹底解説 TypeScript

TypeScript の条件型(Conditional Types)は、型レベルで「もし〜なら〜型、そうでなければ〜型」という条件分岐を表現する機能です。T extends U ? X : Y という構文で書き、型の柔軟な変換・フィルタリング・抽出が可能になります。

条件型は TypeScript 2.8(2018年)で導入され、ExcludeReturnTypeNonNullable など多くの組み込みユーティリティ型の実装にも使われています。本記事では基本構文から分配条件型・infer キーワード・ユーティリティ型の実装読解まで、実践パターンを含めて完全解説します。

この記事でわかること

  • 条件型 T extends U ? X : Y の基本構文と動作原理
  • 分配条件型(Distributive Conditional Types)の仕組みと制御方法
  • infer キーワードで型パターンから型を抽出する方法
  • never を使った型フィルタリングのパターン
  • ExcludeExtractReturnType 等の組み込み型の実装解読
  • 実践パターン3本・FAQ6問
スポンサーリンク

1. 条件型の基本構文

条件型の構文は JavaScript の三項演算子と似ています。T extends U ? X : Y は「TU に代入可能なら X、そうでなければ Y」を意味します。

// 基本構文
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<"hello">; // true("hello" は string のサブタイプ)

// 具体的な型を条件で切り替え
type ToArray<T> = T extends any[] ? T : T[];

type D = ToArray<string>;    // string[]
type E = ToArray<number[]>;  // number[](既に配列なのでそのまま)

// null/undefined を除外する
type NonNullish<T> = T extends null | undefined ? never : T;

type F = NonNullish<string | null | undefined>; // string
// ※ これは組み込み NonNullable<T> と同じ動作
extends の意味:「代入可能か」
条件型の extends は「継承」ではなく「代入可能か(assignable)」を意味します。string extends string | number は true(string は string | number に代入可能)、string | number extends string は false(string | number は string だけには代入できない)。この区別が条件型を正しく理解する鍵です。

1-1. ネストした条件型

// 型の「大きさ」を判定する型
type TypeCategory<T> =
    T extends string  ? "string-like"  :
    T extends number  ? "number-like"  :
    T extends boolean ? "boolean-like" :
    T extends object  ? "object-like"  :
    "other";

type Cat1 = TypeCategory<"hello">;    // "string-like"
type Cat2 = TypeCategory<42>;         // "number-like"
type Cat3 = TypeCategory<string[]>;   // "object-like"
type Cat4 = TypeCategory<symbol>;     // "other"

// 読み取り専用配列かどうかを判定
type IsReadonlyArray<T> =
    T extends readonly any[]
        ? T extends any[]
            ? false  // mutable 配列
            : true   // readonly 配列
        : false;     // 配列でない

type R1 = IsReadonlyArray<readonly string[]>; // true
type R2 = IsReadonlyArray<string[]>;          // false
type R3 = IsReadonlyArray<string>;            // false

2. 分配条件型(Distributive Conditional Types)

条件型に Union 型を渡すと、各メンバーに対して条件型が個別に適用され、結果が Union として結合されます。これを分配条件型と呼びます。

// Union 型を渡すと分配される
type IsString<T> = T extends string ? true : false;

type Result = IsString<string | number | boolean>;
// = IsString<string> | IsString<number> | IsString<boolean>
// = true | false | false
// = boolean

// Exclude<T, U> の仕組み:分配 + never によるフィルタ
type MyExclude<T, U> = T extends U ? never : T;

type E1 = MyExclude<"a" | "b" | "c", "b">;
// = ("a" extends "b" ? never : "a") | ("b" extends "b" ? never : "b") | ("c" extends "b" ? never : "c")
// = "a" | never | "c"
// = "a" | "c"

// Extract<T, U> の仕組み:条件を逆にする
type MyExtract<T, U> = T extends U ? T : never;

type E2 = MyExtract<"a" | "b" | 1 | 2, string>;
// = "a" | "b"(string に代入可能なメンバーのみ残る)

2-1. 分配を抑制する:タプルで包む

分配を意図的に無効化したい場合は、型パラメータを [T] のようにタプルで包みます。

// 分配あり(Union を個別処理)
type WithDistribute<T> = T extends string ? "yes" : "no";
type D1 = WithDistribute<string | number>; // "yes" | "no"

// 分配なし(Union をそのまま評価)
type WithoutDistribute<T> = [T] extends [string] ? "yes" : "no";
type D2 = WithoutDistribute<string | number>; // "no"(string | number は string に代入不可)

// 実用例:T が never かどうかを確認
type IsNever<T> = [T] extends [never] ? true : false;

type N1 = IsNever<never>;          // true
type N2 = IsNever<string>;         // false
type N3 = IsNever<string | never>; // false(string | never = string)

// ※ [T] なしだと IsNever<never> は never になる(分配で never に収束)
type BrokenIsNever<T> = T extends never ? true : false;
type Broken = BrokenIsNever<never>; // never(バグ:true にならない)
never を条件型に渡すと never が返る
T extends U ? X : YT = never を渡すと、分配条件型の仕組みにより「0個の Union に分配」されて結果は never になります。IsNever<T> を作る場合は [T] extends [never] とタプルで包んで分配を抑制することが必須です。

3. infer キーワードで型を抽出する

infer は条件型の extends 節内でのみ使えるキーワードで、型パターンにマッチした部分を新しい型変数として取り出します。関数の戻り値型・引数型・配列要素型などを動的に抽出するのに使います。

// 関数の戻り値型を取得(ReturnType の実装)
type MyReturnType<T extends (...args: any[]) => any> =
    T extends (...args: any[]) => infer R ? R : never;

function greet(name: string): string { return `Hello, ${name}`; }
type GreetReturn = MyReturnType<typeof greet>; // string

// 関数の第1引数の型を取得
type FirstParam<T extends (...args: any[]) => any> =
    T extends (first: infer F, ...rest: any[]) => any ? F : never;

function fetchUser(id: number, token: string): void {}
type FP = FirstParam<typeof fetchUser>; // number

// Promise の解決値の型を取得(Awaited の実装)
type MyAwaited<T> =
    T extends Promise<infer U> ? MyAwaited<U> : T;

type AW1 = MyAwaited<Promise<string>>;          // string
type AW2 = MyAwaited<Promise<Promise<number>>>; // number(再帰で解決)
type AW3 = MyAwaited<string>;                   // string(Promise でない)

3-1. 配列・タプルへの infer 適用

// 配列の要素型を取得
type ElementType<T> = T extends (infer E)[] ? E : never;

type ET1 = ElementType<string[]>;          // string
type ET2 = ElementType<(number | boolean)[]>; // number | boolean

// タプルの先頭要素型を取得
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;

type H1 = Head<[string, number, boolean]>; // string
type H2 = Head<[number]>;                  // number
type H3 = Head<[]>;                        // never

// タプルの末尾を取得(Tail)
type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;

type T1 = Tail<[string, number, boolean]>; // [number, boolean]
type T2 = Tail<[string]>;                  // []

// タプルの最後の要素型を取得(Last)
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;

type L1 = Last<[string, number, boolean]>; // boolean
type L2 = Last<[string]>;                  // string
infer が使えるのは extends 節の右辺のみ
infer RT extends SomeType<infer R> の形でのみ有効です。定義した R は then 節(? の後)でのみ参照でき、else 節(: の後)では使えません。条件型の外で infer を書くとコンパイルエラーになります。

3-2. クラスと Union 型への infer 適用

// Constructor の引数型を取得(ConstructorParameters の実装)
type MyConstructorParams<T extends new (...args: any[]) => any> =
    T extends new (...args: infer P) => any ? P : never;

class HttpClient {
    constructor(baseUrl: string, timeout: number, headers: Record<string, string>) {}
}
type CP = MyConstructorParams<typeof HttpClient>;
// [baseUrl: string, timeout: number, headers: Record<string, string>]

// InstanceType の実装
type MyInstanceType<T extends new (...args: any[]) => any> =
    T extends new (...args: any[]) => infer I ? I : never;

type Instance = MyInstanceType<typeof HttpClient>; // HttpClient

// Union 内の各関数の戻り値型を Union にまとめる
type UnionReturnTypes<T extends (...args: any[]) => any> =
    T extends (...args: any[]) => infer R ? R : never;

type Handlers =
    | ((id: number) => string)
    | ((id: number) => boolean)
    | ((id: number) => number[]);

type AllReturns = UnionReturnTypes<Handlers>;
// string | boolean | number[]

4. never による型フィルタリング

never は「あり得ない型」を表し、Union に含まれると自動的に除去されます。条件型と組み合わせることで、特定の条件を満たさない型を Union から除外できます。

// 関数型プロパティのキーのみを抽出
type FunctionKeys<T> = {
    [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];

interface Service {
    name: string;
    version: number;
    getUser(id: number): Promise<User>;
    updateUser(id: number, data: Partial<User>): Promise<void>;
    deleteUser(id: number): Promise<void>;
}

type ServiceMethods = FunctionKeys<Service>;
// "getUser" | "updateUser" | "deleteUser"

// 逆:非関数プロパティのキーのみを抽出
type NonFunctionKeys<T> = {
    [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
}[keyof T];

type ServiceProps = NonFunctionKeys<Service>;
// "name" | "version"

// 特定の型のプロパティのみ抽出(PickByValue)
type PickByValue<T, V> = {
    [K in keyof T as T[K] extends V ? K : never]: T[K];
};

interface Config {
    host: string;
    port: number;
    debug: boolean;
    timeout: number;
    name: string;
}

type StringConfig = PickByValue<Config, string>;
// { host: string; name: string }

type NumberConfig = PickByValue<Config, number>;
// { port: number; timeout: number }

5. 組み込みユーティリティ型の実装解読

TypeScript の組み込みユーティリティ型の多くは条件型で実装されています。実装を理解することで条件型の使い方が深く理解できます。

ユーティリティ型 実装 動作
Exclude<T, U> T extends U ? never : T T から U に代入可能なメンバーを除外
Extract<T, U> T extends U ? T : never T から U に代入可能なメンバーのみ残す
NonNullable<T> T extends null | undefined ? never : T null と undefined を除外
ReturnType<T> T extends (...args: any) => infer R ? R : any 関数型 T の戻り値型を取得
Parameters<T> T extends (...args: infer P) => any ? P : never 関数型 T の引数型をタプルで取得
InstanceType<T> T extends new (...args: any) => infer R ? R : any コンストラクタ型 T のインスタンス型を取得
Awaited<T> 再帰的条件型 Promise<T> を再帰的にアンラップ
// これらの実際の使用例
type Status = "active" | "inactive" | "pending" | "deleted";

// Exclude: 特定の状態を除外
type ActiveStatus = Exclude<Status, "deleted" | "inactive">;
// "active" | "pending"

// Extract: 特定の状態のみ抽出
type EndStatus = Extract<Status, "inactive" | "deleted" | "archived">;
// "inactive" | "deleted"("archived" は Status にないので除外)

// ReturnType + Parameters で関数シグネチャを再利用
type ApiHandler = (req: Request, res: Response, next: NextFunction) => void;
type HandlerParams = Parameters<ApiHandler>;
// [req: Request, res: Response, next: NextFunction]

function withLogger(handler: ApiHandler): ApiHandler {
    return (...args: Parameters<ApiHandler>) => {
        console.log("Request received");
        return handler(...args);
    };
}

ユーティリティ型の詳細な使い方は ユーティリティ型完全ガイド を参照してください。

6. 実践パターン3本

実践例1:DeepRequired – ネストしたオブジェクトを再帰的に必須化

標準の Required<T> は浅い変換のみです。条件型と再帰型を組み合わせることで、深いネストにも対応できます。

// DeepRequired: すべてのプロパティを再帰的に必須にする
type DeepRequired<T> = T extends object
    ? { [K in keyof T]-?: DeepRequired<T[K]> }
    : T;

interface UserSettings {
    profile?: {
        name?: string;
        avatar?: string;
        preferences?: {
            theme?: "light" | "dark";
            language?: string;
            notifications?: boolean;
        };
    };
    privacy?: {
        publicProfile?: boolean;
        showEmail?: boolean;
    };
}

type RequiredSettings = DeepRequired<UserSettings>;
// すべてのネストした ? が除去される
// {
//   profile: {
//     name: string;
//     avatar: string;
//     preferences: {
//       theme: "light" | "dark";
//       language: string;
//       notifications: boolean;
//     }
//   };
//   privacy: { publicProfile: boolean; showEmail: boolean };
// }

// -? は optional を除去するモディファイア(readonly の -readonly と同じ仕組み)
// T extends object ? ... : T で配列・関数を object として扱わないよう注意
// 配列を除外したい場合:
type DeepRequiredSafe<T> =
    T extends (infer E)[]
        ? DeepRequiredSafe<E>[]
        : T extends object
            ? { [K in keyof T]-?: DeepRequiredSafe<T[K]> }
            : T;

実践例2:型安全な Redux アクション型の自動生成

アクションクリエイターのオブジェクトから、条件型を使ってディスパッチ可能なアクション型を自動生成するパターンです。

// アクションクリエイター定義
const actions = {
    setUser:    (user: { id: number; name: string }) => ({ type: "SET_USER"    as const, payload: user }),
    clearUser:  ()                                   => ({ type: "CLEAR_USER"  as const }),
    setLoading: (loading: boolean)                   => ({ type: "SET_LOADING" as const, payload: loading }),
    setError:   (error: string | null)               => ({ type: "SET_ERROR"   as const, payload: error }),
} as const;

// typeof actions の各プロパティ(関数)の戻り値型を Union にまとめる
type ActionCreators = typeof actions;
type AppAction = ReturnType<ActionCreators[keyof ActionCreators]>;
// = ReturnType<typeof actions.setUser>
//   | ReturnType<typeof actions.clearUser>
//   | ReturnType<typeof actions.setLoading>
//   | ReturnType<typeof actions.setError>

// reducer に型付け
interface AppState {
    user: { id: number; name: string } | null;
    loading: boolean;
    error: string | null;
}

function reducer(state: AppState, action: AppAction): AppState {
    switch (action.type) {
        case "SET_USER":    return { ...state, user: action.payload };
        case "CLEAR_USER":  return { ...state, user: null };
        case "SET_LOADING": return { ...state, loading: action.payload };
        case "SET_ERROR":   return { ...state, error: action.payload };
        // default の exhaustive check は never で担保
        default: const _: never = action; return state;
    }
}

// アクションクリエイターを追加するだけで AppAction が自動的に更新される

switch 文の網羅性チェック(never による exhaustive check)の詳細は 判別可能なユニオン型完全ガイド を参照してください。

実践例3:型レベルでのバリデーション(数値範囲・文字列長の型チェック)

条件型と再帰型を組み合わせて、実行時ではなくコンパイル時にバリデーションを行う高度なパターンです。

// タプル長を使って数値の範囲を型レベルで表現
type BuildTuple<N extends number, T extends any[] = []> =
    T["length"] extends N ? T : BuildTuple<N, [...T, unknown]>;

// N が Min 以上 Max 以下かどうかを型で確認
type InRange<N extends number, Min extends number, Max extends number> =
    BuildTuple<N> extends [...BuildTuple<Min>, ...any[]]
        ? BuildTuple<Max> extends [...BuildTuple<N>, ...any[]]
            ? true
            : false
        : false;

type R1 = InRange<5, 1, 10>;   // true(5 は 1〜10 の範囲)
type R2 = InRange<0, 1, 10>;   // false(0 は範囲外)
type R3 = InRange<10, 1, 10>;  // true(境界値は含む)

// 型安全な年齢入力(0〜150 の整数のみ許可)
type ValidAge<N extends number> = InRange<N, 0, 150> extends true ? N : never;

function setAge<N extends number>(age: ValidAge<N>): void {
    console.log(`Age: ${age}`);
}

// setAge(25);   // OK(コンパイル時に 25 は 0〜150 の範囲と確認)
// setAge(200);  // Error: ValidAge<200> が never になるため

// ※ この技法は小さな数値にのみ実用的(大きな N は型チェックが重くなる)

Mapped Types と組み合わせたパターンは 高度な型完全ガイドinfer を使った型推論の詳細は 型推論完全ガイド を参照してください。

7. まとめ:使い分け早見表

やりたいこと 条件型パターン 代表例
型の条件分岐 T extends U ? X : Y IsString<T>・型カテゴリ分類
Union のフィルタ T extends U ? never : T Exclude<T, U>NonNullable<T>
Union の絞り込み T extends U ? T : never Extract<T, U>・特定型のみ残す
分配の抑制 [T] extends [U] ? X : Y IsNever<T>・全体としての評価
型の抽出(infer) T extends (...) => infer R ? R : never ReturnTypeAwaited・配列要素型
プロパティフィルタ T[K] extends V ? K : never PickByValue・関数キーの抽出
再帰的型変換 条件型 + 再帰 DeepRequiredDeepReadonly

条件型は TypeScript の型システムで最も表現力の高い機能の一つです。最初は ExcludeExtract の実装を読み解くところから始め、次に infer での型抽出、最後に分配条件型の制御という順番で習得するのがおすすめです。組み込みユーティリティ型の実装を理解すると、カスタム型ユーティリティの作成も自然にできるようになります。

FAQ

QT extends U と T = U(代入)の違いは何ですか?

AT extends U は「TU代入可能か」という制約または条件判定です。T = U はデフォルト型引数の設定です。条件型の extends は部分型関係(structural subtyping)を確認するため、string extends string | number は true ですが string extends string も true です。

Q分配条件型が意図せず動いてしまいます。どう対処すればよいですか?

AUnion 型を渡したときに分配を避けたい場合は、型パラメータを [T] extends [U] とタプルで包んで分配を無効化してください。また、意図的に分配させたくないケースとして「T = never の判定」が代表的です。IsNever<T> の実装では必ず [T] extends [never] を使います。

Qinfer は条件型の外で使えませんか?

Ainfer は条件型の extends 節の右辺(T extends X<infer U> ? ... の形)でのみ使えます。条件型の外では使えません。また、infer で定義した型変数は then 節(? の後)でのみ参照でき、else 節(: の後)では参照できません。

Q条件型で循環参照エラーが出ます。どうすれば解決できますか?

A再帰的な条件型は TypeScript のコンパイラが一定の深さで打ち切ります。多くの場合、基底ケース(終了条件)が正しく書けていないことが原因です。例えば DeepRequired<T>T extends object を使う際、DateRegExpFunction 等も object に含まれるため、除外しないと意図しない再帰になることがあります。T extends Date | RegExp | Function ? T : ... で先に除外してください。

Qnever 型はいつ Union から消えますか?

Anever は「空の Union」を表すため、他の型と Union を取ると自動的に消えます。string | never = string"a" | never | "b" = "a" | "b" となります。この性質を利用して条件型でフィルタリングを行うのが Exclude などの実装原理です。すべてのメンバーが never になった場合、Union 全体が never になります。

Q条件型のパフォーマンスが心配です。型チェックが遅くなりますか?

A複雑な条件型・再帰的な条件型・大きな Union への分配条件型は型チェックを遅くする可能性があります。対策として①再帰の深さを制限する②Union の数を減らす③interface の宣言マージを活用して型の計算をキャッシュさせる、などがあります。tsc --diagnostics でどの型が遅いか確認できます。

条件型をマスターすると、TypeScript の型システムの表現力を最大限に活かせます。まずは ExcludeReturnType の実装を手元で書いてみることから始めてください。