WithCodeMedia-1-pc
previous arrowprevious arrow
next arrownext arrow

WithCodeMedia-1-sp
previous arrowprevious arrow
next arrownext arrow

TypeScript Conditional Type完全ガイド|T extends U・Union Distribution・infer・再帰的条件型・組み込みユーティリティ型の実装・実践カスタム型まで徹底解説

目次

この記事でわかること

  • Conditional Type(T extends U ? X : Y)の基本構文と型階層の関係
  • 分配的条件型(Union Distribution)の仕組みと分配の抑制方法
  • inferキーワードを使った関数・Promise・タプルからの型抽出
  • DeepPartial・DeepReadonly・Flattenなど再帰的条件型の実装
  • よくある落とし穴(never・anyの挙動)と型テストの書き方
生徒

TypeScriptの型でif文みたいな条件分岐をしたいんですが……Conditional Typeというのを聞いたことがあるんですが、難しそうで

ペン博士

Conditional TypeはTypeScriptの強力な機能の一つじゃ!三項演算子のような形で「型がAならXの型を返し、そうでなければYの型を返す」という条件分岐が型レベルでできるんじゃ。inferキーワードで型を「変数として捕捉」することで、ReturnTypeやExtractなどの組み込みユーティリティ型も自作できるようになるし、DeepPartialのような再帰的な型も実装できるぞ!

結論:Conditional Type(T extends U ? X : Y)はTypeScriptで型レベルの条件分岐を実現する機能です。Union Distributionでユニオン型の各メンバーに個別適用でき、inferキーワードで型を変数として捕捉できます。ReturnType・Extract・Excludeなどの組み込みユーティリティ型もすべてこの機能で実装されています。

本記事では、Conditional Typeの基本構文・分配的条件型と非分配的条件型・Union Distribution(ユニオンの分配)・inferキーワードの多様な応用・全組み込みユーティリティ型の自作・再帰的条件型(DeepPartial・DeepReadonly・Flatten)・テンプレートリテラル型との組み合わせ・実践的なカスタムユーティリティ型・よくある落とし穴と解決策を完全解説します。


Conditional Typeの基本構文

>// 基本構文: T extends U ? X : Y
// T が U のサブタイプ(U に代入可能)であれば X を、そうでなければ Y を返す

// シンプルな例
type IsNumber = T extends number ? true : false

type T1 = IsNumber<10>       // true
type T2 = IsNumber<'hello'>  // false
type T3 = IsNumber   // true
type T4 = IsNumber   // false

// TypeScriptの型階層(サブタイプの関係)
// number extends number       → true
// 42 extends number           → true(リテラル型はnumberのサブタイプ)
// string extends number       → false
// string extends string|number → true(stringはstring|numberのサブタイプ)
// never extends string        → true(neverはすべての型のサブタイプ)
// string extends unknown      → true(すべての型はunknownのサブタイプ)

// ネストした条件型
type TypeName =
  T extends string   ? 'string'   :
  T extends number   ? 'number'   :
  T extends boolean  ? 'boolean'  :
  T extends null     ? 'null'     :
  T extends undefined? 'undefined':
  T extends object   ? 'object'   :
  'unknown'

type TN1 = TypeName<'hello'>    // 'string'
type TN2 = TypeName<42>         // 'number'
type TN3 = TypeName       // 'boolean'
type TN4 = TypeName       // 'null'
type TN5 = TypeName<{}>         // 'object'
type TN6 = TypeName     // 'unknown'

分配的条件型(Distributive Conditional Types)

Conditional Typeには「分配」という重要な性質があります。型変数TにUnion型が渡されると、Union型の各メンバーに対して個別にConditional Typeが適用されます。

>// 分配的条件型の例

type ToArray = T extends any ? T[] : never

// TにUnion型を渡すと分配が起きる
type StringOrNumberArray = ToArray
// → string[] | number[]
// (string | number)[] とは異なる!

// 内部動作のイメージ:
// string extends any ? string[] : never → string[]
// number extends any ? number[] : never → number[]
// 結果: string[] | number[]

// ユニオンを分配してフィルタリング
type FilterBoolean = T extends boolean ? never : T

type Mixed = string | number | boolean | null
type NonBoolean = FilterBoolean
// → string | number | null(booleanが除去される)

// 分配が発生するのは「型変数」として裸で使われる場合のみ
// tuple/arrayでラップすると分配を抑制できる
type IsNever = [T] extends [never] ? true : false
//                ↑ ラップで分配を抑制

type Test1 = IsNever         // true
type Test2 = IsNever        // false
type Test3 = IsNever // false(neverはunionから消えるため)

// 比較: 分配的な場合
type IsNeverDistributive = T extends never ? true : false
type TestD1 = IsNeverDistributive  // never(分配でneverは何も適用されない!)

Union Distributionの活用例|Extract・Exclude・NonNullable

>// ===== 組み込みユーティリティ型の自作 =====

// Extract: T からUのサブタイプのみを抽出
type MyExtract = T extends U ? T : never

type JobGrade = 'S' | 'A' | 'B' | 'C' | 'D'
type HighGrade = MyExtract
// → "S" | "A"

// Exclude: T からUのサブタイプを除外
type MyExclude = T extends U ? never : T

type LowGrade = MyExclude
// → "B" | "C" | "D"

// NonNullable: null と undefined を除外
type MyNonNullable = T extends null | undefined ? never : T

type MaybeString = string | null | undefined
type DefiniteString = MyNonNullable  // string

// Filter(汎用フィルター型)
type Filter = T extends U ? T : never  // = Extract と同じ
type Reject = T extends U ? never : T  // = Exclude と同じ

// 特定の型のプロパティキーのみを抽出(実用的!)
type PickByValue = {
  [K in keyof T as T[K] extends V ? K : never]: T[K]
}

interface User {
  id: number
  name: string
  email: string
  age: number
  isAdmin: boolean
}

type StringKeys = PickByValue
// → { name: string; email: string }

type NumberKeys = PickByValue
// → { id: number; age: number }

inferキーワード|型を「推論して抽出」する

infer は Conditional Type の条件部で使えるキーワードで、パターンマッチした型を変数として捕捉できます。「型レベルの正規表現キャプチャ」のようなイメージです。

関数型からのinfer活用

>// ===== 関数型に関するinfer =====

// ReturnTypeの自作(関数の戻り値型を抽出)
type MyReturnType = T extends (...args: any[]) => infer R ? R : never

type Fn1 = () => number
type Fn2 = (x: string) => string[]
type Fn3 = () => Promise<{ id: number }>
type AsyncFn = () => Promise

type R1 = MyReturnType  // number
type R2 = MyReturnType  // string[]
type R3 = MyReturnType  // Promise<{ id: number }>
type R4 = MyReturnType  // never(関数でないため)

// Parametersの自作(関数のパラメーター型をTupleで取得)
type MyParameters = T extends (...args: infer P) => any ? P : never

type P1 = MyParameters<(x: string, y: number) => void>  // [string, number]
type P2 = MyParameters<() => void>                       // []
type P3 = MyParameters<(ids: number[]) => void>          // [number[]]

// FirstParam(最初の引数の型のみ取得)
type FirstParam = T extends (first: infer F, ...rest: any[]) => any ? F : never
type FP = FirstParam<(id: number, name: string) => void>  // number

// ConstructorParametersの自作(コンストラクターのパラメーター型)
type MyConstructorParameters = T extends new (...args: infer P) => any ? P : never

class MyClass {
  constructor(public name: string, public value: number) {}
}
type CP = MyConstructorParameters  // [string, number]

// InstanceTypeの自作(クラスのインスタンス型を取得)
type MyInstanceType = T extends new (...args: any[]) => infer I ? I : never
type IT = MyInstanceType  // MyClass

Promise・配列・オブジェクトへのinfer活用

>// ===== 配列・Promiseからのinfer =====

// Promiseの中の型を取り出す(Awaitedの自作)
type UnwrapPromise = T extends Promise ? UnwrapPromise : T
// 再帰的にPromiseをアンラップ

type P1 = UnwrapPromise>                   // string
type P2 = UnwrapPromise>>          // number
type P3 = UnwrapPromise>>> // boolean
type P4 = UnwrapPromise                            // string(Promiseでないのでそのまま)

// Awaited(TypeScript 4.5以降の組み込み型)
type A1 = Awaited>  // string(組み込みで同じ動作)

// 配列の要素型を取り出す
type ElementType = T extends ReadonlyArray ? E : never

type E1 = ElementType          // string
type E2 = ElementType // number
type E3 = ElementType<[string, number]>  // string | number(tupleの場合)

// タプル操作
type Head = T extends [infer H, ...any[]] ? H : never
type Tail = T extends [any, ...infer T] ? T : never
type Last = T extends [...any[], infer L] ? L : never
type Init = T extends [...infer I, any] ? I : never

type Tuple = [string, number, boolean, null]
type H = Head   // string
type Ta = Tail  // [number, boolean, null]
type L = Last   // null
type I = Init   // [string, number, boolean]

// ===== オブジェクト型からのinfer =====

// Promiseの戻り値の型を関数から直接取得
type AsyncReturnType = T extends (...args: any[]) => Promise ? R : never

async function fetchUser(id: number): Promise<{ id: number; name: string }> {
  return { id, name: 'Taro' }
}
type UserType = AsyncReturnType
// → { id: number; name: string }

// プロパティの型を取得
type PropType = T[K] extends infer V ? V : never

type UserName = PropType<{ id: number; name: string }, 'name'>  // string

再帰的条件型|DeepPartial・DeepReadonly・Flatten

TypeScript 4.1以降、再帰的な条件型がサポートされました。これにより、ネストした型を深く変換するユーティリティ型が実装できます。

>// DeepPartial: 深いレベルまで全プロパティをoptionalにする
type DeepPartial = T extends object
  ? { [K in keyof T]?: DeepPartial }
  : T

// 組み込みのPartialは1レベルのみ
type ShallowPartial = Partial<{
  user: { name: string; address: { city: string } }
}>
// → { user?: { name: string; address: { city: string } } }
// user.name や user.address.city はoptionalにならない

type DeepPartialExample = DeepPartial<{
  user: { name: string; address: { city: string; zip: string } }
  settings: { theme: 'light' | 'dark'; fontSize: number }
}>
// → { user?: { name?: string; address?: { city?: string; zip?: string } }; settings?: ... }

// 使い方(フォームの部分的な更新に便利)
function updateUser(patch: DeepPartial): void {
  // patchは全プロパティがoptional(深いレベルでも)
}

// ===== DeepReadonly =====
type DeepReadonly = T extends (infer U)[]
  ? ReadonlyArray>
  : T extends object
  ? { readonly [K in keyof T]: DeepReadonly }
  : T

type Config = {
  database: {
    host: string
    port: number
    credentials: { username: string; password: string }
  }
  features: string[]
}

type ImmutableConfig = DeepReadonly
// 全プロパティ・ネストされたオブジェクト・配列がreadonly

// ===== Flatten(配列をフラット化する型)=====
type Flatten = T extends Array ? Flatten : T

type F1 = Flatten    // string
type F2 = Flatten        // number
type F3 = Flatten          // string(配列でないのでそのまま)

// ===== DeepOmit(深いネストのプロパティを除外)=====
// 単純な実装例
type DeepOmit = T extends object
  ? { [P in Exclude]: DeepOmit }
  : T

type WithoutPassword = DeepOmit<{
  user: { id: number; name: string; password: string }
  admin: { id: number; password: string; role: string }
}, 'password'>
// → { user: { id: number; name: string }; admin: { id: number; role: string } }

テンプレートリテラル型との組み合わせ

>// テンプレートリテラル型 + Conditional Type + infer

// Split: 文字列型をデリミタで分割してTupleに変換
type Split =
  S extends `${infer T}${D}${infer U}`
    ? [T, ...Split]
    : [S]

type S1 = Split<'a.b.c', '.'>  // ["a", "b", "c"]
type S2 = Split<'hello-world', '-'>  // ["hello", "world"]
type S3 = Split<'no-delimiter', ','>  // ["no-delimiter"]

// Join: Tupleを文字列に結合
type Join =
  T extends [infer F extends string, ...infer R extends string[]]
    ? `${F}${R extends [] ? '' : `${D}${Join}`}`
    : ''

type J1 = Join<['a', 'b', 'c'], '.'>  // "a.b.c"

// CamelCase → snake_case 変換
type CamelToSnake =
  T extends `${infer Head}${infer Tail}`
    ? Tail extends Uncapitalize
      ? `${Lowercase}${CamelToSnake}`
      : `${Lowercase}_${CamelToSnake}`
    : T

// ※注意: 上記は簡易実装。実際は大文字のパターンが複雑

// Paths(ネストしたオブジェクトの全パスを抽出)
type Paths =
  T extends object
    ? { [K in keyof T & string]:
        P extends '' ? K | `${K}.${Paths}`
                     : `${K}` | `${K}.${Paths}`
      }[keyof T & string]
    : never

type Config = {
  user: { name: string; address: { city: string; zip: string } }
  theme: string
}

type ConfigPaths = Paths
// "user" | "user.name" | "user.address" | "user.address.city" | "user.address.zip" | "theme"

// フォームやgetterの実装に使えるパターン
function getConfigValue(obj: T, path: Paths): unknown {
  // pathに基づいてネストした値を取得
  return path.split('.').reduce((acc, key) => (acc as any)?.[key], obj)
}

実践的なカスタムユーティリティ型

>// ===== APIレスポンス用のユーティリティ型 =====

// APIレスポンスの成功/失敗型から実際のデータ型を取り出す
type ApiResponse =
  | { status: 'success'; data: T }
  | { status: 'error'; code: string; message: string }

type ExtractData = R extends { status: 'success'; data: infer D } ? D : never
type ExtractError = R extends { status: 'error' } ? R : never

type UserResponse = ApiResponse<{ id: number; name: string }>
type UserData = ExtractData
// → { id: number; name: string }

// リクエスト/レスポンスの型を安全に扱う
type ApiResult =
  | { ok: true; data: T }
  | { ok: false; error: Error }

function isOk(result: ApiResult): result is { ok: true; data: T } {
  return result.ok === true
}

// ===== フォーム用ユーティリティ型 =====

// オブジェクトの全キーを文字列でパス化
type FieldPath = {
  [K in keyof T]: K extends string
    ? T[K] extends Record
      ? K | `${K}.${FieldPath}`
      : K
    : never
}[keyof T]

type FormValues = {
  user: { name: string; email: string }
  age: number
  address: { city: string; zip: string }
}
type FormPaths = FieldPath
// "user" | "user.name" | "user.email" | "age" | "address" | "address.city" | "address.zip"

// ===== コンポーネント設計用ユーティリティ型 =====

// PropsWithChildren の汎用版
type WithChildren = T & { children?: React.ReactNode }

// className を optional に追加
type WithClassName = T & { className?: string }

// 特定のプロパティをオーバーライド可能にする
type Override = Omit & U

// aタグかbuttonタグかを型で切り替えるポリモーフィックコンポーネント
type PolymorphicRef =
  React.ComponentPropsWithRef['ref']

type PolymorphicProps = {
  as?: T
  ref?: PolymorphicRef
} & Props & Omit, keyof Props | 'as'>

// ===== 状態管理用ユーティリティ型 =====

// イベントハンドラー型を自動生成
type EventHandlers = {
  [K in keyof T as K extends string ? `on${Capitalize}` : never]?: (value: T[K]) => void
}

type State = { count: number; name: string; active: boolean }
type Handlers = EventHandlers
// → { onCount?: (value: number) => void; onName?: (value: string) => void; onActive?: (value: boolean) => void }

よくある落とし穴と解決策

>// ===== よくある落とし穴 =====

// 落とし穴1: never と Union Distribution
type IsNeverWrong = T extends never ? true : false
type WrongResult = IsNeverWrong  // never!(trueにならない)

// 理由: never がUnion Distributionに渡されると何も分配されずneverが返る
// 解決: [] でラップして分配を抑制する
type IsNeverCorrect = [T] extends [never] ? true : false
type CorrectResult = IsNeverCorrect  // true ✅

// 落とし穴2: any は全ての extends を true にする
type IsString = T extends string ? true : false
type AnyResult = IsString  // boolean(true | false)

// any を渡すと条件が不定になりboolean型になる
// 解決: anyを特別扱いする
type IsStringStrict = [T] extends [string] ? true : false
// これでもanyはtrueになってしまう場合があるため、anyチェックを加える

// 落とし穴3: Union Distributionが意図しない分配を起こす場合
type WrapInArray = T extends any ? [T] : never
type WrappedUnion = WrapInArray
// → [string] | [number](分配で別々のタプルになる)

// 分配を抑制したい場合
type WrapInArrayNoDistribute = [T] extends [any] ? [T] : never
type Wrapped = WrapInArrayNoDistribute
// → [string | number](Union全体をひとつのタプルに)

// 落とし穴4: inferの共変・反変
// 関数の引数はinferで取り出すとUnionではなくIntersectionになる
type FnUnion = ((x: string) => void) | ((x: number) => void)
type ParamsUnion = FnUnion extends (...args: infer P) => void ? P : never
// → [x: string & number](Intersectionになる!)

// 解決: Union型の引数を別々に扱う場合は個別に処理する

型テストの書き方

>// ===== 型テストの方法 =====

// 方法1: @ts-expect-error を使ったテスト
const test: string = '' as unknown as string
// @ts-expect-error: string は number に代入不可
const shouldFail: number = test  // エラーが出ることを確認

// 方法2: Expect + Equal 型ユーティリティ(type-festライブラリ)
type Expect = T
type Equal = (() => T extends X ? 1 : 2) extends (() => T extends Y ? 1 : 2) ? true : false

type TestSuite = [
  Expect string>, string>>,
  Expect, 'a'>>,
  Expect>, number>>,
  Expect, 1>>,
]
// ↑ 全て true になれば型テスト通過

// 方法3: tsd ライブラリ(型テスト専用ツール)
// npm install --save-dev tsd
import { expectType, expectError } from 'tsd'

expectType('' as MyReturnType<() => string>)
expectType<'a' | 'b'>('' as MyExtract<'a' | 'b' | 'c', 'a' | 'b'>)

// 型テストを CI/CD に組み込んで型の正確性を保証

よくある質問(FAQ)

Q1. Conditional Typeと型ガードはどう使い分けますか?

A. Conditional Type は型定義の時点で(コンパイル時に)型を条件分岐させるものです。型ガードはランタイムで条件をチェックしてTypeScriptに「この分岐の中ではこの型だ」と教えるものです。function isString(x: unknown): x is string { return typeof x === 'string' } のような型ガードは実行時に動作し、Conditional Typeは型計算の際にのみ評価されます。両者を組み合わせて使うことが多く、Conditional Typeで「ある関数が返す型を計算」し、型ガードで「実行時にその型かどうかを確認」するパターンが実践的です。

Q2. 再帰的な条件型でTypeScriptが遅くなる場合はどうすればいいですか?

A. 再帰的な型は深すぎると 「Type instantiation is excessively deep」 エラーが出ます。解決策は①再帰の深さを制限するカウンターを型パラメーターで持つ②interfaceを使った遅延評価③そもそも深い再帰が必要な場合は実行時処理に切り替えるの3つです。TypeScriptのコンパイラーは型の再帰深さに上限(デフォルト1000)があるため、実際のプロジェクトデータの深さに合わせた設計が重要です。

Q3. inferで推論された型の範囲はどう制限しますか?

A. TypeScript 4.7以降、infer R extends string のようにinferに extends制約を付けることができます。type FirstString<T> = T extends [infer F extends string, ...any[]] ? F : never のように書くと、Fはstringのサブタイプとしてキャプチャされます。4.7以前は推論後に別のConditional Typeで制約をかける必要がありました。

Q4. Conditional TypeとMapped Typeはどう使い分けますか?

A. Mapped Type({ [K in keyof T]: ... }はオブジェクトの各プロパティを変換するのに使います。Conditional Typeは型そのものを条件で切り替えるのに使います。実際には両者を組み合わせることが多く、Mapped TypeのKey Remapping(as節)でConditional Typeを使って特定キーを除外・リネームするパターンが強力です。例えば [K in keyof T as T[K] extends string ? K : never] のように書いて文字列型のプロパティだけを抽出できます。

Q5. never型が条件型の中で「消える」のはなぜですか?

A. TypeScriptでは A | neverA と同等です。Union型にneverが含まれると自動的に取り除かれます。また分配的条件型では、neverを型変数に渡すと「分配するメンバーが0個」という状態になるため、結果もneverになります。これはExcludeなどのユーティリティ型で意図的に利用されています(除外したい型をneverにすることでUnionから取り除く)。


まとめ

  • 基本構文T extends U ? X : Y でTがUのサブタイプかを判定して型を分岐させる。
  • Union Distribution(分配):Tが型変数でUnion型の場合、各メンバーに個別に条件型が適用される。ExtractやExcludeはこれを利用。
  • 分配の抑制[T] extends [U] のようにタプルでラップすると分配を抑制。neverのチェックに重要。
  • infer:条件部でパターンマッチした型を変数として捕捉。ReturnType・Parameters・UnwrapPromise等の実装に活用。
  • 組み込み型の自作:Exclude・Extract・NonNullable・Parameters・ConstructorParameters・InstanceType全てConditional Typeで実装されている。
  • 再帰的条件型:TypeScript 4.1以降で対応。DeepPartial・DeepReadonly・Flattenなどの深い変換型が実装可能。
  • テンプレートリテラル型との組み合わせ:Split・Join・CamelToSnake・Pathsなど強力な文字列型操作が可能。
  • よくある落とし穴:neverのDistribution・anyの挙動・関数の引数のIntersection化に注意。

Conditional TypeはTypeScriptの「型をプログラムする」機能の中核です。まずはReturnType・Extract・Excludeを自作して動作を体感してから、inferを使ったUnwrapPromiseやDeepPartialの実装に挑戦してみましょう。型パズルを楽しみながら学ぶと理解が深まります。


WithCodeを体験できる初級コース公開中!

この記事を書いた人

WithCode(ウィズコード)は「目指すなら稼げる人材」をビジョンに、累計400名以上のフリーランスを輩出してきた超実践型プログラミングスクールです。150社以上の実案件支援を特徴にWeb制作・Webデザインなどの役立つ情報を現場のノウハウに基づいて発信していきます。

– service –WithGroupの運営サービス

  • WithCode
    - ウィズコード -

    スクール

    「未経験」から
    現場で通用する
    スキルを身に付けよう!

    詳細はこちら
  • WithFree
    - ウィズフリ -

    実案件サポート

    制作会社のサポート下で
    実務経験を積んでいこう!

    詳細はこちら
  • WithCareer
    - ウィズキャリ -

    就転職サポート

    大手エージェントのサポート下で
    キャリアアップを目指そう!

    詳細はこちら

公式サイト より
今すぐ
無料カウンセリング
予約!

目次