TypeScriptジェネリック条件、マッピング、推論のマスター
Grace Collins
Solutions Engineer · Leapcell

基本ジェネリックを超えてTypeScriptの可能性を解き放つ
TypeScriptの型システムは信じられないほど強力であり、そのジェネリック機能は、柔軟で再利用可能なコードを構築するための礎です。基本的なジェネリックを使用すると、さまざまなデータ型を扱える関数やクラスを作成できますが、条件付き型、マッピング型、「infer」キーワードのような高度な機能に深く踏み込むと、真の魔法が解き放たれます。これらの構造は、入力に基づいて動的に適応する型を定義することを可能にし、非常に堅牢で表現力豊かで保守性の高いコードベースにつながります。型安全性とコード品質が最優先される、ますます複雑化するWeb開発の状況において、これらの高度なジェネリックテクニックを習得することは、もはや贅沢ではなく、真剣なTypeScript開発者にとっての必要不可欠です。この記事では、これらの強力な機能の謎を解き明かし、そのpracticalな応用を例示し、それらを最大限に活用するためのガイドを提供します。
高度なジェネリック型の解明
条件付き型、マッピング型、「infer」の複雑な部分に進む前に、TypeScriptにおける型とジェネリックが基本的に何を意味するのかを簡単に確認しましょう。本質的に、型は値の形状と動作を記述します。一方、ジェネリックは、型安全性に妥協することなく柔軟性を提供し、任意の型を扱える関数、クラス、インターフェイスを作成できる型変数のようなものです。それでは、高度な概念を探っていきましょう。
条件付き型
条件付き型は、型関係を評価する条件に基づいて2つの異なる型から選択することを可能にします。主に extends
キーワードを使用し、JavaScriptの三項演算子に似た構文を持ちます:SomeType extends OtherType ? TrueType : FalseType
。
原則: 核となる考え方は、型レベルのチェックを実行することです。SomeType
が OtherType
に代入可能である場合(つまり、SomeType
が OtherType
のサブタイプであるか、それに等しい場合)、TrueType
が選択されます。それ以外の場合は、FalseType
が選択されます。
実装と応用: 入力が実際に関数である場合にのみ、関数の戻り型を抽出したいシナリオを考えてみましょう。
type NonFunction = string | number | boolean; type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : NonFunction; // 使用例: function sum(a: number, b: number): number { return a + b; } type SumReturnType = GetReturnType<typeof sum>; // number const myString = "hello"; type StringReturnType = GetReturnType<typeof myString>; // NonFunction (myStringのtypeofは関数ではないため) function greet(name: string): void { console.log(`Hello, ${name}`); } type GreetReturnType = GetReturnType<typeof greet>; // void
この例では、GetReturnType<T>
は条件付き型です。これは T
が (...args: any[]) => infer R
を拡張するかどうかをチェックします。T
が関数型の場合、infer R
キーワード(後で説明します)はその戻り型をキャプチャし、R
が選択されます。それ以外の場合は、NonFunction
が選択されます。これは、他の型を操作する型安全なユーティリティ関数を作成するのに非常に役立ちます。
別の一般的なユースケースは型フィルタリングです:
type FilterString<T> = T extends string ? T : never; type MixedTuple = [1, "hello", true, "world"]; type OnlyStringsFromTuple = FilterString<MixedTuple[number]>; // "hello" | "world"
ここでは、MixedTuple[number]
は 1 | "hello" | true | "world"
のユニオンを作成します。その後 FilterString
はユニオン内の各型を反復処理し、string
型のみが条件を通過し、"hello" | "world"
が生成されます。
マッピング型
マッピング型は、既存のオブジェクト型のプロパティを新しいオブジェクト型に変換することを可能にします。基本的に、指定された型のキーを反復処理し、各プロパティの型に変換を適用します。
原則: 主要な構文には、K
がプロパティキーのユニオン(通常は keyof SomeType
から取得)である [P in K]
を使用してキーを反復処理することが含まれます。マッピング内では、プロパティ名(キーのリマッピングには as
を使用)および/またはその型を変更できます。
実装と応用:
一般的なシナリオは、オブジェクトのすべてのプロパティを readonly
または optional
にすることです。TypeScriptは Partial<T>
や Readonly<T>
のような組み込みマッピング型を提供していますが、それらの基盤となるメカニズムを理解することは、独自の型を作成するために不可欠です。
type Coordinates = { x: number; y: number; z: number; }; // 例1:すべてのプロパティをnullableにする type Nullable<T> = { [P in keyof T]: T[P] | null; }; type NullableCoordinates = Nullable<Coordinates>; /* type NullableCoordinates = { x: number | null; y: number | null; z: number | null; } */ // 例2:すべてのプロパティをoptionalかつreadonlyにする type DeepReadonlyAndOptional<T> = { readonly [P in keyof T]?: T[P]; }; type ReadonlyOptionalCoordinates = DeepReadonlyAndOptional<Coordinates>; /* type ReadonlyOptionalCoordinates = { readonly x?: number; readonly y?: number; readonly z?: number; } */
as
を使用したキーのリマッピング:
マッピング型はプロパティ名を変更することもできます。これはデータ構造を変換するのに強力です。
type User = { id: string; name: string; email: string; }; // キーを大文字に変換 type UppercaseKeys<T> = { [P in keyof T as Uppercase<P & string>]: T[P]; }; type UserUppercaseKeys = UppercaseKeys<User>; /* type UserUppercaseKeys = { ID: string; NAME: string; EMAIL: string; } */ // キーを "id" を削除して "Ref" を追加するように変換 type PropRef<T> = { [P in keyof T as P extends `${infer K}Id` ? `${K}Ref` : P]: T[P]; }; type Product = { productId: string; name: string }; type ProductRef = PropRef<Product>; /* type ProductRef = { productRef: string; name: string; } */
キーのリマッピングは、型レベルでの複雑なデータ移行やAPIコントラクト変換の可能性を広げます。
infer
キーワード
infer
キーワードは、常に条件付き型の extends
句内で使用されます。その目的は、チェックされる型から「推論」できる新しい型変数を宣言することです。
原則: infer
は、型チェック中に他の型内の特定の場所からTypeScriptが推測する型のプレースホルダーとして機能します。一度推論されると、この新しい型変数は条件付き型のTrueType
ブランチで使用できます。
実装と応用:
GetReturnType<T>
で infer
をすでに見てきました。配列型やPromise型に関するより多くの例、特に見ていきましょう。
配列要素型の推論:
type GetArrayElementType<T> = T extends (infer U)[] ? U : never; type Numbers = number[]; type ElementOfNumbers = GetArrayElementType<Numbers>; // number type Strings = string[]; type ElementOfStrings = GetArrayElementType<Strings>; // string type NotAnArray = string; type ElementOfNotAnArray = GetArrayElementType<NotAnArray>; // never
ここでは、T extends (infer U)[]
が T
が配列型であるかをチェックします。もしそうなら、infer U
はその配列内の要素の型をキャプチャし、U
が結果になります。
Promise解決値の推論:
type GetPromiseResolvedType<T> = T extends Promise<infer U> ? U : T; type MyPromise = Promise<string>; type PromiseResult = GetPromiseResolvedType<MyPromise>; // string type AnotherPromise = Promise<number[]>; type AnotherPromiseResult = GetPromiseResolvedType<AnotherPromise>; // number[] type NotAPromise = boolean; type NotAPromiseResult = GetPromiseResolvedType<NotAPromise>; // boolean
このユーティリティ型は、非同期コードを扱う際に非常に役立ち、.then()
ハンドラーを正しく型付けすることを可能にします。
関数パラメータの推論:
type GetFunctionParameters<T> = T extends (...args: infer P) => any ? P : never; function doSomething(name: string, age: number): string { return `Name: ${name}, Age: ${age}`; } type DoSomethingParams = GetFunctionParameters<typeof doSomething>; // [name: string, age: number] type SomeOtherFunction = (a: boolean) => void; type SomeOtherFunctionParams = GetFunctionParameters<SomeOtherFunction>; // [a: boolean]
これにより、高階関数やモックに役立つ、指定された関数のパラメータの完全なタプルを抽出できます。
概念の結合:実際の例
これらの概念を組み合わせて、より高度な型を作成しましょう。APIエンドポイントのセットが関数として定義されていると想像してください。それらがPromiseである場合はそれらの戻り型を抽出し、それ以外の場合はそのまま保持したいとします。
type APIResponseMapping = { getUser: (id: string) => Promise<{ id: string; name: string }>; getProducts: () => Promise<Array<{ id: string; name: string; price: number }>>; logEvent: (event: string) => void; // Promiseではない }; // Promiseの解決済み型、または型自体を取得するユーティリティ type UnpackPromise<T> = T extends Promise<infer U> ? U : T; // APIエンドポイントの戻り型を変換するマッピング型 type ResolvedAPIResponses<T> = { [K in keyof T]: T[K] extends (...args: any[]) => infer R ? UnpackPromise<R> : never; }; type ProcessedResponses = ResolvedAPIResponses<APIResponseMapping>; /* type ProcessedResponses = { getUser: { id: string; name: string; }; getProducts: { id: string; name: string; price: number; }[]; logEvent: void; } */
この高度な例では、ResolvedAPIResponses
は APIResponseMapping
のキーを反復処理するマッピング型です。各プロパティ K
について、まず条件付き型を使用して T[K]
が関数であるかどうかをチェックします。もしそうなら、戻り型 R
を infer
します。次に、UnpackPromise
条件付き型を R
に適用して最終的な解決済み型を取得します。T[K]
が関数でない場合は、never
にデフォルト設定されます。これは、これらの高度なジェネリック機能が連鎖して、非常に具体的で有用な型変換を作成できることを示しています。
精密さの力
条件付き型、マッピング型、「infer」キーワードは、単なる学術的な好奇心ではありません。真に型安全で、柔軟で、保守性の高いTypeScriptアプリケーションを作成するための不可欠なツールです。これらは、複雑な型関係を表現し、型レベルでデータ形状を変換し、特定の型情報を抽出することを可能にし、堅牢であり、扱うのが楽しいコードにつながります。これらの高度なジェネリックテクニックを習得することは、基本的な型安全性から、強力な型駆動開発の領域へとあなたを導き、実行時にしか現れないエラーをコンパイル時にキャッチすることを可能にします。これらの概念を受け入れ、プロジェクトでTypeScriptの可能性を最大限に引き出しましょう。