



WithCodeMedia-1-pc
WithCodeMedia-2-pc
WithCodeMedia-3-pc
WithCodeMedia-4-pc




WithCodeMedia-1-sp
WithCodeMedia-2-sp
WithCodeMedia-3-sp
WithCodeMedia-4-sp









生徒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)・テンプレートリテラル型との組み合わせ・実践的なカスタムユーティリティ型・よくある落とし穴と解決策を完全解説します。
>// 基本構文: T extends U ? X : Y
// T が U のサブタイプ(U に代入可能)であれば X を、そうでなければ Y を返す
// シンプルな例
type IsNumber<T> = T extends number ? true : false
type T1 = IsNumber<10> // true
type T2 = IsNumber<'hello'> // false
type T3 = IsNumber<number> // true
type T4 = IsNumber<string> // 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> =
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<true> // 'boolean'
type TN4 = TypeName<null> // 'null'
type TN5 = TypeName<{}> // 'object'
type TN6 = TypeName<symbol> // 'unknown'Conditional Typeには「分配」という重要な性質があります。型変数TにUnion型が渡されると、Union型の各メンバーに対して個別にConditional Typeが適用されます。
>// 分配的条件型の例
type ToArray<T> = T extends any ? T[] : never
// TにUnion型を渡すと分配が起きる
type StringOrNumberArray = ToArray<string | number>
// → string[] | number[]
// (string | number)[] とは異なる!
// 内部動作のイメージ:
// string extends any ? string[] : never → string[]
// number extends any ? number[] : never → number[]
// 結果: string[] | number[]
// ユニオンを分配してフィルタリング
type FilterBoolean<T> = T extends boolean ? never : T
type Mixed = string | number | boolean | null
type NonBoolean = FilterBoolean<Mixed>
// → string | number | null(booleanが除去される)
// 分配が発生するのは「型変数」として裸で使われる場合のみ
// tuple/arrayでラップすると分配を抑制できる
type IsNever<T> = [T] extends [never] ? true : false
// ↑ ラップで分配を抑制
type Test1 = IsNever<never> // true
type Test2 = IsNever<string> // false
type Test3 = IsNever<string | never> // false(neverはunionから消えるため)
// 比較: 分配的な場合
type IsNeverDistributive<T> = T extends never ? true : false
type TestD1 = IsNeverDistributive<never> // never(分配でneverは何も適用されない!)>// ===== 組み込みユーティリティ型の自作 =====
// Extract<T, U>: T からUのサブタイプのみを抽出
type MyExtract<T, U> = T extends U ? T : never
type JobGrade = 'S' | 'A' | 'B' | 'C' | 'D'
type HighGrade = MyExtract<JobGrade, 'S' | 'A'>
// → "S" | "A"
// Exclude<T, U>: T からUのサブタイプを除外
type MyExclude<T, U> = T extends U ? never : T
type LowGrade = MyExclude<JobGrade, 'S' | 'A'>
// → "B" | "C" | "D"
// NonNullable<T>: null と undefined を除外
type MyNonNullable<T> = T extends null | undefined ? never : T
type MaybeString = string | null | undefined
type DefiniteString = MyNonNullable<MaybeString> // string
// Filter(汎用フィルター型)
type Filter<T, U> = T extends U ? T : never // = Extract と同じ
type Reject<T, U> = T extends U ? never : T // = Exclude と同じ
// 特定の型のプロパティキーのみを抽出(実用的!)
type PickByValue<T, V> = {
[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<User, string>
// → { name: string; email: string }
type NumberKeys = PickByValue<User, number>
// → { id: number; age: number }infer は Conditional Type の条件部で使えるキーワードで、パターンマッチした型を変数として捕捉できます。「型レベルの正規表現キャプチャ」のようなイメージです。
>// ===== 関数型に関するinfer =====
// ReturnType<T>の自作(関数の戻り値型を抽出)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type Fn1 = () => number
type Fn2 = (x: string) => string[]
type Fn3 = () => Promise<{ id: number }>
type AsyncFn = () => Promise<string>
type R1 = MyReturnType<Fn1> // number
type R2 = MyReturnType<Fn2> // string[]
type R3 = MyReturnType<Fn3> // Promise<{ id: number }>
type R4 = MyReturnType<string> // never(関数でないため)
// Parameters<T>の自作(関数のパラメーター型をTupleで取得)
type MyParameters<T> = 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> = T extends (first: infer F, ...rest: any[]) => any ? F : never
type FP = FirstParam<(id: number, name: string) => void> // number
// ConstructorParameters<T>の自作(コンストラクターのパラメーター型)
type MyConstructorParameters<T> = T extends new (...args: infer P) => any ? P : never
class MyClass {
constructor(public name: string, public value: number) {}
}
type CP = MyConstructorParameters<typeof MyClass> // [string, number]
// InstanceType<T>の自作(クラスのインスタンス型を取得)
type MyInstanceType<T> = T extends new (...args: any[]) => infer I ? I : never
type IT = MyInstanceType<typeof MyClass> // MyClass>// ===== 配列・Promiseからのinfer =====
// Promiseの中の型を取り出す(Awaited<T>の自作)
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T
// 再帰的にPromiseをアンラップ
type P1 = UnwrapPromise<Promise<string>> // string
type P2 = UnwrapPromise<Promise<Promise<number>>> // number
type P3 = UnwrapPromise<Promise<Promise<Promise<boolean>>>> // boolean
type P4 = UnwrapPromise<string> // string(Promiseでないのでそのまま)
// Awaited<T>(TypeScript 4.5以降の組み込み型)
type A1 = Awaited<Promise<string>> // string(組み込みで同じ動作)
// 配列の要素型を取り出す
type ElementType<T> = T extends ReadonlyArray<infer E> ? E : never
type E1 = ElementType<string[]> // string
type E2 = ElementType<readonly number[]> // number
type E3 = ElementType<[string, number]> // string | number(tupleの場合)
// タプル操作
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never
type Tail<T extends any[]> = T extends [any, ...infer T] ? T : never
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never
type Init<T extends any[]> = T extends [...infer I, any] ? I : never
type Tuple = [string, number, boolean, null]
type H = Head<Tuple> // string
type Ta = Tail<Tuple> // [number, boolean, null]
type L = Last<Tuple> // null
type I = Init<Tuple> // [string, number, boolean]
// ===== オブジェクト型からのinfer =====
// Promiseの戻り値の型を関数から直接取得
type AsyncReturnType<T> = T extends (...args: any[]) => Promise<infer R> ? R : never
async function fetchUser(id: number): Promise<{ id: number; name: string }> {
return { id, name: 'Taro' }
}
type UserType = AsyncReturnType<typeof fetchUser>
// → { id: number; name: string }
// プロパティの型を取得
type PropType<T, K extends keyof T> = T[K] extends infer V ? V : never
type UserName = PropType<{ id: number; name: string }, 'name'> // stringTypeScript 4.1以降、再帰的な条件型がサポートされました。これにより、ネストした型を深く変換するユーティリティ型が実装できます。
>// DeepPartial: 深いレベルまで全プロパティをoptionalにする
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T
// 組み込みのPartial<T>は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<User>): void {
// patchは全プロパティがoptional(深いレベルでも)
}
// ===== DeepReadonly =====
type DeepReadonly<T> = T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T
type Config = {
database: {
host: string
port: number
credentials: { username: string; password: string }
}
features: string[]
}
type ImmutableConfig = DeepReadonly<Config>
// 全プロパティ・ネストされたオブジェクト・配列がreadonly
// ===== Flatten(配列をフラット化する型)=====
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T
type F1 = Flatten<string[][][]> // string
type F2 = Flatten<number[]> // number
type F3 = Flatten<string> // string(配列でないのでそのまま)
// ===== DeepOmit(深いネストのプロパティを除外)=====
// 単純な実装例
type DeepOmit<T, K extends string | number | symbol> = T extends object
? { [P in Exclude<keyof T, K>]: DeepOmit<T[P], K> }
: 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 string, D extends string> =
S extends `${infer T}${D}${infer U}`
? [T, ...Split<U, D>]
: [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 string[], D extends string> =
T extends [infer F extends string, ...infer R extends string[]]
? `${F}${R extends [] ? '' : `${D}${Join<R, D>}`}`
: ''
type J1 = Join<['a', 'b', 'c'], '.'> // "a.b.c"
// CamelCase → snake_case 変換
type CamelToSnake<T extends string> =
T extends `${infer Head}${infer Tail}`
? Tail extends Uncapitalize<Tail>
? `${Lowercase<Head>}${CamelToSnake<Tail>}`
: `${Lowercase<Head>}_${CamelToSnake<Tail>}`
: T
// ※注意: 上記は簡易実装。実際は大文字のパターンが複雑
// Paths(ネストしたオブジェクトの全パスを抽出)
type Paths<T, P extends string = ''> =
T extends object
? { [K in keyof T & string]:
P extends '' ? K | `${K}.${Paths<T[K], K>}`
: `${K}` | `${K}.${Paths<T[K], K>}`
}[keyof T & string]
: never
type Config = {
user: { name: string; address: { city: string; zip: string } }
theme: string
}
type ConfigPaths = Paths<Config>
// "user" | "user.name" | "user.address" | "user.address.city" | "user.address.zip" | "theme"
// フォームやgetterの実装に使えるパターン
function getConfigValue<T extends Config>(obj: T, path: Paths<T>): unknown {
// pathに基づいてネストした値を取得
return path.split('.').reduce((acc, key) => (acc as any)?.[key], obj)
}>// ===== APIレスポンス用のユーティリティ型 =====
// APIレスポンスの成功/失敗型から実際のデータ型を取り出す
type ApiResponse<T> =
| { status: 'success'; data: T }
| { status: 'error'; code: string; message: string }
type ExtractData<R> = R extends { status: 'success'; data: infer D } ? D : never
type ExtractError<R> = R extends { status: 'error' } ? R : never
type UserResponse = ApiResponse<{ id: number; name: string }>
type UserData = ExtractData<UserResponse>
// → { id: number; name: string }
// リクエスト/レスポンスの型を安全に扱う
type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: Error }
function isOk<T>(result: ApiResult<T>): result is { ok: true; data: T } {
return result.ok === true
}
// ===== フォーム用ユーティリティ型 =====
// オブジェクトの全キーを文字列でパス化
type FieldPath<T> = {
[K in keyof T]: K extends string
? T[K] extends Record<string, any>
? K | `${K}.${FieldPath<T[K]>}`
: K
: never
}[keyof T]
type FormValues = {
user: { name: string; email: string }
age: number
address: { city: string; zip: string }
}
type FormPaths = FieldPath<FormValues>
// "user" | "user.name" | "user.email" | "age" | "address" | "address.city" | "address.zip"
// ===== コンポーネント設計用ユーティリティ型 =====
// PropsWithChildren の汎用版
type WithChildren<T = {}> = T & { children?: React.ReactNode }
// className を optional に追加
type WithClassName<T = {}> = T & { className?: string }
// 特定のプロパティをオーバーライド可能にする
type Override<T, U> = Omit<T, keyof U> & U
// aタグかbuttonタグかを型で切り替えるポリモーフィックコンポーネント
type PolymorphicRef<T extends React.ElementType> =
React.ComponentPropsWithRef<T>['ref']
type PolymorphicProps<T extends React.ElementType, Props = {}> = {
as?: T
ref?: PolymorphicRef<T>
} & Props & Omit<React.ComponentPropsWithoutRef<T>, keyof Props | 'as'>
// ===== 状態管理用ユーティリティ型 =====
// イベントハンドラー型を自動生成
type EventHandlers<T> = {
[K in keyof T as K extends string ? `on${Capitalize<K>}` : never]?: (value: T[K]) => void
}
type State = { count: number; name: string; active: boolean }
type Handlers = EventHandlers<State>
// → { onCount?: (value: number) => void; onName?: (value: string) => void; onActive?: (value: boolean) => void }>// ===== よくある落とし穴 =====
// 落とし穴1: never と Union Distribution
type IsNeverWrong<T> = T extends never ? true : false
type WrongResult = IsNeverWrong<never> // never!(trueにならない)
// 理由: never がUnion Distributionに渡されると何も分配されずneverが返る
// 解決: [] でラップして分配を抑制する
type IsNeverCorrect<T> = [T] extends [never] ? true : false
type CorrectResult = IsNeverCorrect<never> // true ✅
// 落とし穴2: any は全ての extends を true にする
type IsString<T> = T extends string ? true : false
type AnyResult = IsString<any> // boolean(true | false)
// any を渡すと条件が不定になりboolean型になる
// 解決: anyを特別扱いする
type IsStringStrict<T> = [T] extends [string] ? true : false
// これでもanyはtrueになってしまう場合があるため、anyチェックを加える
// 落とし穴3: Union Distributionが意図しない分配を起こす場合
type WrapInArray<T> = T extends any ? [T] : never
type WrappedUnion = WrapInArray<string | number>
// → [string] | [number](分配で別々のタプルになる)
// 分配を抑制したい場合
type WrapInArrayNoDistribute<T> = [T] extends [any] ? [T] : never
type Wrapped = WrapInArrayNoDistribute<string | number>
// → [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 extends true> = T
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false
type TestSuite = [
Expect<Equal<MyReturnType<() => string>, string>>,
Expect<Equal<MyExtract<'a' | 'b' | 'c', 'a'>, 'a'>>,
Expect<Equal<UnwrapPromise<Promise<number>>, number>>,
Expect<Equal<Head<[1, 2, 3]>, 1>>,
]
// ↑ 全て true になれば型テスト通過
// 方法3: tsd ライブラリ(型テスト専用ツール)
// npm install --save-dev tsd
import { expectType, expectError } from 'tsd'
expectType<string>('' as MyReturnType<() => string>)
expectType<'a' | 'b'>('' as MyExtract<'a' | 'b' | 'c', 'a' | 'b'>)
// 型テストを CI/CD に組み込んで型の正確性を保証A. Conditional Type は型定義の時点で(コンパイル時に)型を条件分岐させるものです。型ガードはランタイムで条件をチェックしてTypeScriptに「この分岐の中ではこの型だ」と教えるものです。function isString(x: unknown): x is string { return typeof x === 'string' } のような型ガードは実行時に動作し、Conditional Typeは型計算の際にのみ評価されます。両者を組み合わせて使うことが多く、Conditional Typeで「ある関数が返す型を計算」し、型ガードで「実行時にその型かどうかを確認」するパターンが実践的です。
A. 再帰的な型は深すぎると 「Type instantiation is excessively deep」 エラーが出ます。解決策は①再帰の深さを制限するカウンターを型パラメーターで持つ②interfaceを使った遅延評価③そもそも深い再帰が必要な場合は実行時処理に切り替えるの3つです。TypeScriptのコンパイラーは型の再帰深さに上限(デフォルト1000)があるため、実際のプロジェクトデータの深さに合わせた設計が重要です。
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で制約をかける必要がありました。
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] のように書いて文字列型のプロパティだけを抽出できます。
A. TypeScriptでは A | never は A と同等です。Union型にneverが含まれると自動的に取り除かれます。また分配的条件型では、neverを型変数に渡すと「分配するメンバーが0個」という状態になるため、結果もneverになります。これはExcludeなどのユーティリティ型で意図的に利用されています(除外したい型をneverにすることでUnionから取り除く)。
T extends U ? X : Y でTがUのサブタイプかを判定して型を分岐させる。[T] extends [U] のようにタプルでラップすると分配を抑制。neverのチェックに重要。Conditional TypeはTypeScriptの「型をプログラムする」機能の中核です。まずはReturnType・Extract・Excludeを自作して動作を体感してから、inferを使ったUnwrapPromiseやDeepPartialの実装に挑戦してみましょう。型パズルを楽しみながら学ぶと理解が深まります。


目的に合わせて選べる「AI副業」「AI転職」「AI活用」の3コースを用意。副収入・キャリアチェンジ・日常の生産性アップまで、あなたのゴールに合わせてAIを学べます。
会員登録はカンタン30秒で完了します。まずは公式LINEから、無料でAI学習をスタートしましょう!
公式サイト より
今すぐ
無料カウンセリング
を予約!