



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 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'
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は何も適用されない!)
>// ===== 組み込みユーティリティ型の自作 =====
// 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 は Conditional Type の条件部で使えるキーワードで、パターンマッチした型を変数として捕捉できます。「型レベルの正規表現キャプチャ」のようなイメージです。
>// ===== 関数型に関する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の中の型を取り出す(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
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 に組み込んで型の正確性を保証
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の実装に挑戦してみましょう。型パズルを楽しみながら学ぶと理解が深まります。
公式サイト より
今すぐ
無料カウンセリング
を予約!