TypeScript Conditional Types完全ガイド〜extends・infer・Distributive・実用パターン25選【2026年版】〜

TypeScriptで「引数が文字列ならstring、配列なら要素型を返したい」「Promise<T>から中身のTを取り出したい」と思ったことはありませんか。これらを実現するのがConditional Types(条件型)です。T extends U ? X : YというJavaScriptの三項演算子そっくりの構文で、型レベルのif文が書けるようになります。

本記事はTS 5.x準拠でConditional Typesを実装レベルで使いこなす実践ガイドとして、40個以上のコピペで動くコードとともに、extends条件分岐・inferによる型抽出・Distributive Conditional Types・実用パターン25選を解説します。ReturnTypeAwaitedExcludeもすべてConditional Typesで実装されており、ここを理解すれば「Utility Typesの中身が読める」「自作Utility Typesが作れる」レベルに到達します。

この記事で得られること

  • T extends U ? X : Yの基本動作と、なぜ「型レベルのif」になるのか
  • inferキーワードによる型抽出のメカニズム(ReturnType/Parameters/Awaitedの内部実装)
  • Distributive Conditional Typesの仕組みと、[T] extends [U]による抑制テクニック
  • Exclude/Extract/NonNullableの自作実装で標準型の正体を見抜く
  • Tuple操作・文字列パース・ドット記法アクセス・型レベル数値演算など応用パターン25選

Conditional Typesとは何か

Conditional Typesは、型を条件分岐させて別の型を返す仕組みです。TypeScript 2.8で導入されて以来、Utility Typesと型推論の土台として欠かせない存在になっています。

三項演算子そっくりの基本構文

もっとも素朴なConditional Typesは「型Tが型Uに代入可能ならXを、そうでなければYを返す」というシンプルな分岐です。三項演算子と同じリズムで読めます。

// 型レベルのif: TがUに代入可能ならX、そうでなければY
type If<T, U, X, Y> = T extends U ? X : Y;

type A = If<"hello", string, "yes", "no">; // "yes"
type B = If<42, string, "yes", "no">;      // "no"
type C = If<true, boolean, 1, 0>;          // 1

extendsはクラスの継承と同じキーワードに見えますが、Conditional Typesでは「左辺が右辺のサブタイプか?(代入可能か?)」という判定演算子として働きます。意味が違うので混同しないようにしましょう。

素朴な型定義との比較

たとえば「引数が配列なら要素型を、そうでなければそのままの型を返す」関数を型レベルで書きたいとき、Conditional Typesがないと表現できません。

// ❌ Conditional Typesを使わない: 配列か単体かで分岐できない
function unwrap<T>(value: T): T {
  return Array.isArray(value) ? value[0] : value;
  // 戻り値型がTのまま = 配列要素型を返す表現ができない
}
// ✅ Conditional Typesを使う: 配列なら要素型、それ以外はそのまま
type Unwrap<T> = T extends (infer U)[] ? U : T;

function unwrap<T>(value: T): Unwrap<T> {
  return (Array.isArray(value) ? value[0] : value) as Unwrap<T>;
}

const a = unwrap([1, 2, 3]);    // a: number
const b = unwrap("hello");      // b: string
const c = unwrap({ id: 1 });    // c: { id: number }

構文の分解

Conditional Typesの基本構文は次の4要素で構成されます。順に意味を確認しておきましょう。

type Sample<T> = T extends U ? X : Y;
//                ↑①  ↑②     ↑③   ↑④
// ① 判定したい型(ユニオン型なら自動展開される=Distributive)
// ② extendsの右辺(サブタイプ判定の基準)
// ③ trueブランチで返す型(inferで局所変数を導入できる)
// ④ falseブランチで返す型(さらにネスト可能)

このうち最も重要なのは①の「ユニオン型が来るとどう振る舞うか」と、③の「inferで部分構造を切り出せる」という2点です。これを押さえれば、ほぼすべてのUtility Typesの内部実装が読めるようになります。

extendsによる条件分岐

まずはinferを使わない単純な分岐から始めます。extendsの挙動だけで実装できる型は意外に多く、ここが応用の出発点です。

プリミティブ判定

// 文字列かどうかを型レベルで判定
type IsString<T> = T extends string ? true : false;

type X = IsString<"hello">; // true
type Y = IsString<42>;      // false
type Z = IsString<string>;  // true

リテラル型"hello"stringのサブタイプなのでtrueになります。extendsは「代入互換性」を見ているという感覚を掴むためのウォームアップに最適です。

ネスト条件で多分岐

// JS的なtypeofを型レベルで表現
type TypeName<T> =
  T extends string  ? "string"  :
  T extends number  ? "number"  :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

type A = TypeName<"hi">;       // "string"
type B = TypeName<42>;         // "number"
type C = TypeName<() => void>; // "function"
type D = TypeName<{ id: 1 }>;  // "object"

三項演算子のネストと同じく、上から順に評価され最初にマッチしたブランチが採用されます。読みやすさのためにインデントを揃えるのがコツです。

オブジェクト構造の判定

// idプロパティを持つ型かどうか
type HasId<T> = T extends { id: unknown } ? true : false;

type A = HasId<{ id: string; name: string }>; // true
type B = HasId<{ name: string }>;             // false
type C = HasId<{ id: number }>;               // true

「型同士の等価判定」を素朴に書く落とし穴

// ❌ これは等価判定にならない(双方向のextendsチェックが必要)
type NaiveEqual<A, B> = A extends B ? true : false;

type X = NaiveEqual<string, string | number>; // true (片方向だけ)
type Y = NaiveEqual<string | number, string>; // false

双方向にextendsを取らないと正しい等価判定になりません。本物のEqualは後述するrecursive conditionalで実装します。

inferで型を抽出する

inferはConditional Typesの中で「ここの位置にある型を取り出して名前を付ける」と宣言する局所変数のようなものです。これが使えるとUtility Typesの世界が一気に開けます。

inferの基本

// 「配列の要素型」を取り出す
type ElementOf<T> = T extends (infer U)[] ? U : never;

type A = ElementOf<number[]>;        // number
type B = ElementOf<string[]>;        // string
type C = ElementOf<{ id: 1 }[]>;     // { id: 1 }
type D = ElementOf<"not array">;     // never

(infer U)[]は「配列パターンにマッチしたら、その要素型をUと呼ぶ」という意味です。マッチしなければfalseブランチ(never)に落ちます。

Promise<T>の中身を取り出す

// Promiseの解決値型を取り出す(1段)
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<number>>; // number
type C = UnwrapPromise<string>;          // string (そのまま)

関数の戻り値型(ReturnType)を自作

// 標準ReturnTypeと同じものを自前実装
type MyReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : never;

type F = (x: number) => string;
type R = MyReturnType<F>; // string

type G = () => { id: number; name: string };
type S = MyReturnType<G>; // { id: number; name: string }

標準ライブラリlib.es5.d.tsのReturnTypeも、ほぼこの定義そのままです。infer Rを戻り値位置に置くだけで型が抜き出せるのは、改めて見ると驚異的な表現力です。

関数引数(Parameters)を自作

// 引数の型をタプルとして取り出す
type MyParameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

type F = (id: number, name: string) => void;
type P = MyParameters<F>; // [id: number, name: string]

ConstructorParametersとInstanceType

// new シグネチャから引数とインスタンス型を取り出す
type MyConstructorParameters<T extends new (...args: any) => any> =
  T extends new (...args: infer P) => any ? P : never;

type MyInstanceType<T extends new (...args: any) => any> =
  T extends new (...args: any) => infer R ? R : never;

class User {
  constructor(public id: number, public name: string) {}
}

type CP = MyConstructorParameters<typeof User>; // [id: number, name: string]
type IT = MyInstanceType<typeof User>;          // User

Awaited(再帰的Promise剥がし)

// Promiseの入れ子もすべて剥がすTS 4.5+のAwaited
type MyAwaited<T> =
  T extends null | undefined ? T :
  T extends object & { then(onfulfilled: infer F, ...args: any): any }
    ? F extends (value: infer V, ...args: any) => any
      ? MyAwaited<V>
      : never
    : T;

type A = MyAwaited<Promise<string>>;                 // string
type B = MyAwaited<Promise<Promise<number>>>;        // number
type C = MyAwaited<Promise<Promise<Promise<42>>>>;   // 42

thenableを再帰的に追いかけてPromise<Promise>のような入れ子も最後まで剥がすのがミソです。実務ではasync/awaitの戻り値型推論を支える縁の下の力持ちになっています。

Distributive Conditional Types

Conditional Typesには「ユニオン型が来ると自動的に各要素ごとに分配されて評価される」という重要な性質があります。これがDistributive Conditional Typesです。最初は驚きますが、知っていると強力です。

分配の挙動

// T extends string でユニオンを分配
type ToArray<T> = T extends any ? T[] : never;

type A = ToArray<string>;             // string[]
type B = ToArray<string | number>;    // string[] | number[]  ← 分配されている!
type C = ToArray<1 | 2 | 3>;          // 1[] | 2[] | 3[]

素朴に考えると(string | number)[]になりそうですが、ユニオン型を渡すと各要素ごとに評価され、結果もユニオンで返ってきます。これがDistributiveです。

Exclude / Extract / NonNullableを自作

// Excludeの自作実装(分配を利用して該当要素だけneverに落とす)
type MyExclude<T, U> = T extends U ? never : T;

type A = MyExclude<"a" | "b" | "c", "a">; // "b" | "c"
type B = MyExclude<string | number, string>; // number
// Extractの自作実装(分配を利用して該当要素だけ残す)
type MyExtract<T, U> = T extends U ? T : never;

type A = MyExtract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
type B = MyExtract<string | number | boolean, boolean>; // boolean
// NonNullableの自作実装(null/undefinedを除去)
type MyNonNullable<T> = T extends null | undefined ? never : T;

type A = MyNonNullable<string | null>;             // string
type B = MyNonNullable<number | undefined | null>; // number

標準のExclude/Extract/NonNullableも内部は分配を利用したワンライナーです。「条件型の分配性質を逆手に取って、不要要素をneverに落として消す」という発想は応用が効きます。

Distributiveを抑制する([T] extends [U])

ユニオンを丸ごと判定したい場合は、分配されると困ります。対象を1要素のtupleで包めば分配が止まります。

// ❌ 分配されるバージョン: 各要素ごとに判定されてしまう
type IsUnion<T> = T extends any ? true : false;
type A = IsUnion<string | number>; // true | true = true (期待と違う)

// ✅ tupleで包んで分配を抑制
type IsTrulyUnion<T, U = T> =
  T extends any ? ([U] extends [T] ? false : true) : never;

type X = IsTrulyUnion<string>;          // false
type Y = IsTrulyUnion<string | number>; // true
// ユニオン全体に対して「すべてが string か」を素直に判定
type IsAllString<T> = [T] extends [string] ? true : false;

type A = IsAllString<"a" | "b">;        // true
type B = IsAllString<"a" | number>;     // false
type C = IsAllString<string | number>;  // false

「分配が嫌なら[T] extends [U]」というイディオムは超頻出です。覚えておくと型エラーで詰まったときの突破口になります。

tuple操作と再帰

TupleもConditional Types + inferで自在に分解できます。再帰と組み合わせれば、配列のような操作が型レベルで実現できます。

Head / Tail / Last

// 先頭要素を取り出す
type Head<T extends readonly unknown[]> =
  T extends readonly [infer H, ...unknown[]] ? H : never;

type A = Head<[1, 2, 3]>;            // 1
type B = Head<["a", "b", "c"]>;      // "a"
type C = Head<[]>;                   // never
// 先頭を除いた残りを取り出す
type Tail<T extends readonly unknown[]> =
  T extends readonly [unknown, ...infer R] ? R : [];

type A = Tail<[1, 2, 3]>;            // [2, 3]
type B = Tail<["a", "b", "c", "d"]>; // ["b", "c", "d"]
type C = Tail<[]>;                   // []
// 末尾要素を取り出す
type Last<T extends readonly unknown[]> =
  T extends readonly [...unknown[], infer L] ? L : never;

type A = Last<[1, 2, 3]>;            // 3
type B = Last<["a", "b", "c"]>;      // "c"

Reverse Tuple

// 再帰でTupleを逆順にする
type Reverse<T extends readonly unknown[]> =
  T extends readonly [infer H, ...infer R] ? [...Reverse<R>, H] : [];

type A = Reverse<[1, 2, 3]>;            // [3, 2, 1]
type B = Reverse<["a", "b", "c", "d"]>; // ["d", "c", "b", "a"]

「先頭を取り出して残りを再帰呼び出し、後ろにくっつける」というFP的な書き方になります。型レベル再帰は内部スタックを使うので、TS 5.xでは1000段以上深くなるとエラーになる点だけ注意してください。

型レベル足し算

// 数値Nに対応する長さN のtupleを作るヘルパ
type BuildTuple<N extends number, T extends unknown[] = []> =
  T["length"] extends N ? T : BuildTuple<N, [...T, unknown]>;

// AとBの足し算: 長さA+Bのtupleを作って length を取る
type Add<A extends number, B extends number> =
  [...BuildTuple<A>, ...BuildTuple<B>]["length"];

type X = Add<3, 4>;  // 7
type Y = Add<10, 5>; // 15
type Z = Add<0, 0>;  // 0

型レベル数値演算は実用というよりトリック寄りですが、「型システムはチューリング完全に近い表現力を持つ」という事実を実感できる代表例です。

文字列リテラル型のパース

TypeScript 4.1で導入されたTemplate Literal Typesinferを組み合わせると、文字列を型レベルでパースできます。APIパスや設定キーの型生成に大活躍します。

文字列分割パターン

// 文字列をセパレータで分割してtupleにする
type Split<S extends string, Sep extends string> =
  S extends `${infer H}${Sep}${infer R}` ? [H, ...Split<R, Sep>] : [S];

type A = Split<"a,b,c", ",">;                // ["a", "b", "c"]
type B = Split<"2026-05-26", "-">;           // ["2026", "05", "26"]
type C = Split<"/users/:id/posts", "/">;     // ["", "users", ":id", "posts"]

パス文字列の解析(Path traversal型)

// "user.profile.name" のようなパスを分解
type SplitPath<S extends string> =
  S extends `${infer H}.${infer R}` ? [H, ...SplitPath<R>] : [S];

type A = SplitPath<"user.profile.name">; // ["user", "profile", "name"]
type B = SplitPath<"id">;                // ["id"]

dot.notation → ネストvalueを取り出す

// "a.b.c" 形式の文字列でネストされた型をたどる
type Get<T, P extends string> =
  P extends `${infer K}.${infer R}`
    ? K extends keyof T
      ? Get<T[K], R>
      : never
    : P extends keyof T
      ? T[P]
      : never;

type User = {
  id: number;
  profile: {
    name: string;
    address: { city: string; zip: string };
  };
};

type A = Get<User, "id">;                    // number
type B = Get<User, "profile.name">;          // string
type C = Get<User, "profile.address.city">;  // string
type D = Get<User, "profile.unknown">;       // never

lodashの_.getのような関数に正しい戻り値型を付けたいときに重宝します。フォームライブラリのname="profile.address.city"のような文字列パスを型安全に扱う基盤にもなります。

高度な実用パターン

ここからは実務でそのまま使える応用パターンを集めました。標準Utility Typesでは届かない領域を埋めるための型たちです。

型レベル等価判定 Equal

// 厳密な型の等価判定(type-challengesでも使われる定番実装)
type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false;

type A = Equal<string, string>;        // true
type B = Equal<string, number>;        // false
type C = Equal<{ a: 1 }, { a: 1 }>;    // true
type D = Equal<any, unknown>;          // false (anyとunknownを区別!)

双方向のジェネリック関数シグネチャを比較することで、anyunknownのような微妙な差まで区別できる本物のEqualになります。型テストには必須のテクニックです。

Union → Tuple変換

// Union型をIntersectionに変換するヘルパ
type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;

// 最後の要素だけ取り出す
type LastInUnion<U> =
  UnionToIntersection<U extends any ? (x: U) => 0 : never> extends (x: infer L) => 0
    ? L : never;

// 末尾を取り除きながら再帰でTupleを構築
type UnionToTuple<U, Last = LastInUnion<U>> =
  [U] extends [never] ? [] : [...UnionToTuple<Exclude<U, Last>>, Last];

type A = UnionToTuple<"a" | "b" | "c">; // ["a", "b", "c"] (順序は環境依存)

Union → Intersection変換

// 上で定義したUnionToIntersectionの実用例
type UTI<U> =
  (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;

type Merged = UTI<{ a: 1 } | { b: 2 } | { c: 3 }>;
// = { a: 1 } & { b: 2 } & { c: 3 }

const m: Merged = { a: 1, b: 2, c: 3 }; // OK

関数引数の反変位置に置くことで型システムが交差型を計算してくれる、というのが原理です。トリッキーですが「ユニオンをまとめてマージしたい」場面で何度も使います。

JSON.stringify戻り値型(Stringifyable判定)

// JSON.stringifyしても情報が落ちないか型レベルで判定
type IsJsonSafe<T> =
  T extends string | number | boolean | null ? true :
  T extends Array<infer U> ? IsJsonSafe<U> :
  T extends Function | Date | undefined ? false :
  T extends object ? { [K in keyof T]: IsJsonSafe<T[K]> }[keyof T] :
  false;

type A = IsJsonSafe<{ id: number; name: string }>;        // true
type B = IsJsonSafe<{ id: number; cb: () => void }>;      // false
type C = IsJsonSafe<{ tags: string[] }>;                  // true

APIエンドポイント型生成

// "/users/:id/posts/:postId" からパラメータ型を生成
type ExtractParams<S extends string> =
  S extends `${string}:${infer P}/${infer Rest}`
    ? { [K in P | keyof ExtractParams<`/${Rest}`>]: string }
    : S extends `${string}:${infer P}`
      ? { [K in P]: string }
      : {};

type A = ExtractParams<"/users/:id">;
// = { id: string }

type B = ExtractParams<"/users/:userId/posts/:postId">;
// = { userId: string; postId: string }
// 型安全なルーティング関数のシグネチャ
declare function navigate<P extends string>(
  path: P,
  params: ExtractParams<P>
): void;

navigate("/users/:id", { id: "123" });                         // OK
navigate("/users/:userId/posts/:postId", { userId: "1", postId: "2" }); // OK
// navigate("/users/:id", {});                                 // ❌ idが必須

express/Next.jsのルートやリンク生成でこのパターンは威力を発揮します。フロントエンドの「URL文字列の中のプレースホルダーと実引数のズレ」を全部コンパイル時に潰せます。

状態マシン型

// 状態と遷移を型で表現
type State =
  | { kind: "idle" }
  | { kind: "loading"; startedAt: number }
  | { kind: "success"; data: string }
  | { kind: "error"; message: string };

// kindで分岐して特定の状態だけ取り出す
type StateOf<S extends State, K extends State["kind"]> =
  S extends { kind: K } ? S : never;

type Loading = StateOf<State, "loading">; // { kind: "loading"; startedAt: number }
type Success = StateOf<State, "success">; // { kind: "success"; data: string }
// 遷移可能な状態だけを許可する型
type Transition =
  | ["idle", "loading"]
  | ["loading", "success"]
  | ["loading", "error"]
  | ["success", "idle"]
  | ["error", "idle"];

type CanTransition<From extends State["kind"], To extends State["kind"]> =
  [From, To] extends Transition ? true : false;

type T1 = CanTransition<"idle", "loading">;   // true
type T2 = CanTransition<"idle", "success">;   // false
type T3 = CanTransition<"loading", "error">;  // true

関数オーバーロード型抽出

// オーバーロードを持つ関数の「最後のシグネチャ」だけ取れるTSの仕様を活用
interface F {
  (x: number): string;
  (x: string): number;
  (x: boolean): boolean;
}

type LastOverloadReturn = ReturnType<F>; // boolean (最後だけ)

// 全部取り出したい場合は条件型を駆使してマニュアルで分解する
type OverloadReturn<T> =
  T extends {
    (...args: infer A1): infer R1;
    (...args: infer A2): infer R2;
    (...args: infer A3): infer R3;
  } ? R1 | R2 | R3 : never;

type AllRet = OverloadReturn<F>; // string | number | boolean

Object.assign return type

// 複数オブジェクトのマージ後の型
type Merge<T extends readonly object[]> =
  T extends readonly [infer H, ...infer R]
    ? R extends readonly object[]
      ? H & Merge<R>
      : H
    : {};

type A = Merge<[{ a: 1 }, { b: 2 }, { c: 3 }]>;
// = { a: 1 } & { b: 2 } & { c: 3 }

declare function assign<T extends readonly object[]>(...objs: T): Merge<T>;

const result = assign({ a: 1 }, { b: "hi" }, { c: true });
// result: { a: 1 } & { b: "hi" } & { c: true }

DeepReadonly(再帰的読み取り専用)

// すべてのネストプロパティをreadonlyにする
type DeepReadonly<T> = {
  readonly [K in keyof T]:
    T[K] extends (...args: any[]) => any ? T[K] :
    T[K] extends object ? DeepReadonly<T[K]> :
    T[K];
};

type Config = {
  db: { host: string; port: number };
  cache: { ttl: number; tags: string[] };
};

type Frozen = DeepReadonly<Config>;
// すべての階層がreadonlyになる

RequiredKeys / OptionalKeys

// 必須キーだけを取り出す
type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];

// 任意キーだけを取り出す
type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];

type Sample = { id: number; name: string; age?: number; email?: string };

type R = RequiredKeys<Sample>; // "id" | "name"
type O = OptionalKeys<Sample>; // "age" | "email"

{} extends Pick<T, K>で「Kがoptionalなら空オブジェクトが代入可能」という性質を利用するイディオムです。フォーム生成・部分更新APIの型を作るときに重宝します。

落とし穴と回避策

Conditional Typesは表現力が高い反面、ハマりどころも多い領域です。実務でよく踏むトラブルと回避策をまとめます。

分配が起きないケース

// ⚠️ 「裸の型パラメータ」でないと分配されない
type WrappedDistribute<T> = { value: T } extends { value: string } ? true : false;

// boolean は内部的に true | false というユニオン
type A = WrappedDistribute<boolean>;
// 「裸でない」ので分配されず、boolean全体が string に extends できるか判定 → false

分配は「T extends ...のTが裸のジェネリック型パラメータのとき」だけ起こります。tupleで包むと止まる仕様はここから派生しています。

never の分配

// neverを渡すと「空ユニオン」として扱われ、分配の結果もneverになる
type ToArray<T> = T extends any ? T[] : never;

type A = ToArray<never>; // never  (期待: never[] ではない!)

// neverを渡しても分配せずに評価したい場合は [T] で包む
type ToArraySafe<T> = [T] extends [any] ? T[] : never;

type B = ToArraySafe<never>; // never[]

any / unknown / never の扱いを誤る

// any はあらゆる型に extends する一方で、あらゆる型から extends されてしまう
type T1 = any extends string ? true : false; // boolean (true | false)
type T2 = string extends any ? true : false; // true
type T3 = unknown extends string ? true : false; // false
type T4 = string extends unknown ? true : false; // true
type T5 = never extends string ? true : false;  // never (分配して空)

anyは「両方向にextends」が成立する特殊な型なので、Conditional Typesの中ではしばしば想定外のbooleanが返ります。型ガードを書くならunknownを起点にするのが安全です。

再帰の深さ制限

// 数値演算のような重い再帰は制限に当たりやすい
type BuildTuple<N extends number, T extends unknown[] = []> =
  T["length"] extends N ? T : BuildTuple<N, [...T, unknown]>;

// 大きすぎる引数は型エラーになる
// type Huge = BuildTuple<1000>; // Type instantiation is excessively deep

TS 5.xでは再帰の深さ上限が約1000段に拡張されましたが、無制限ではありません。実務で型レベル算術が必要になる場面はまれなので、無理に追求せずランタイム計算に逃がす判断も大切です。

Conditional Typesは「遅延評価」される

// ジェネリックの中ではConditional Typesが即時評価されない場合がある
type IsString<T> = T extends string ? true : false;

function check<T>(v: T): IsString<T> {
  // ❌ ここでは T が未確定なので IsString<T> が遅延評価されエラー
  // return (typeof v === "string") as IsString<T>;
  return (typeof v === "string") as any; // 実装側ではasで回避することが多い
}

const a = check("hi");  // IsString<"hi"> = true
const b = check(42);    // IsString<42> = false

ジェネリック関数の実装内でConditional Typesの結果に依存した処理を書くと「コンパイラはまだ判定できない」とエラーになります。実装側ではas anyかオーバーロードで逃がし、外部APIの型だけ厳密にする、という二段構えが現実的です。

学習を加速させるロードマップ

Conditional Typesは概念は単純(三項演算子の型版)ですが、実用に耐える型を書けるようになるには「inferを読める」「分配を制御できる」「再帰の深さを意識できる」という3つの壁を超える必要があります。一人で学ぶより、体系化されたカリキュラム+講師サポートで一気に駆け上がるのが効率的です。

TypeScript型システムを実務レベルで身につけたい方へ

  • TechAcademy フロントエンドコース — TypeScript+Reactで実プロジェクトを作る短期集中型。メンタリングで型設計の悩みを即解消
  • 侍エンジニア — 専属講師が型設計レビューまで伴走。Conditional Typesを使った業務コードのレビューを依頼できる
  • DMM WEBCAMP — 未経験からTypeScript現場レベルへ。転職保証付き
  • レバテック — TypeScript案件多数。Conditional TypesやUtility Typesを書ける人材は単価が伸びやすい領域

まとめ

Conditional Typesは「T extends U ? X : Y」というたった1行の構文でありながら、infer・Distributive・再帰という3つの強力な機能を内包する型システムの心臓部です。本記事で扱った内容を整理すると以下のとおりです。

  • 基本: T extends U ? X : Yは型レベルの三項演算子。リテラル型・オブジェクト構造・ネスト分岐まで自由に書ける
  • infer: パターンマッチの中で型を抜き出す局所変数。ReturnType・Parameters・Awaitedの土台
  • Distributive: ユニオンを自動分配する性質。Exclude/Extract/NonNullableはこれを活用したワンライナー
  • 抑制: [T] extends [U]でtuple包みすると分配が止まる。ユニオン全体の判定で必須
  • 応用: tuple操作・Template Literal Types・型レベル数値演算・API型生成・状態マシン型まで表現可能
  • 注意: never・any・unknownの分配挙動、再帰の深さ制限、ジェネリック内での遅延評価に注意

Conditional Typesを身につけると「any撲滅」のレベルが一段上がり、「ライブラリの型定義が読める」「自社のドメイン型を真に型安全に表現できる」状態に到達します。ぜひ手元のプロジェクトでReturnType・Awaited・Excludeあたりから自作してみて、標準型の正体を体感してください。

関連記事としてジェネリクス完全ガイドUtility Types完全リファレンスMapped Types完全ガイド型推論完全ガイドもあわせて読むと、TypeScript型システムの全体像が立体的に見えてきます。

コメント

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