TypeScriptの'infer'キーワードに関する包括的なガイド。条件付き型と組み合わせて、強力な型抽出と操作を実現する方法を解説します。
TypeScript Inferをマスターする:高度な型操作のための条件付き型抽出
TypeScriptの型システムは非常に強力で、開発者が堅牢で保守性の高いアプリケーションを作成することを可能にします。この力を可能にする重要な機能の1つが、条件付き型と組み合わせて使用されるinfer
キーワードです。この組み合わせは、複雑な型構造から特定の型を抽出するためのメカニズムを提供します。このブログ記事では、infer
キーワードを深く掘り下げ、その機能と高度な使用例を紹介します。APIインタラクションから複雑なデータ構造の操作まで、さまざまなソフトウェア開発シナリオに適用できる実践的な例を検討します。
条件付き型とは?
infer
について詳しく説明する前に、条件付き型について簡単に復習しましょう。TypeScriptの条件付き型を使用すると、JavaScriptの三項演算子と同様に、条件に基づいて型を定義できます。基本的な構文は次のとおりです。
T extends U ? X : Y
これは次のように読みます:「型T
が型U
に割り当て可能であれば、型はX
であり、そうでなければ型はY
である。」
例:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
infer
キーワードの紹介
infer
キーワードは、条件付き型のextends
句内で使用され、チェックされている型から推測できる型変数を宣言します。本質的には、後で使用するために型の部分を「キャプチャ」できます。
基本的な構文:
type MyType<T> = T extends (infer U) ? U : never;
この例では、T
が何らかの型に割り当て可能である場合、TypeScriptはU
の型を推測しようとします。推測が成功した場合、型はU
になります。それ以外の場合はnever
になります。
infer
の簡単な例
1. 関数の戻り値の型を推測する
一般的な使用例は、関数の戻り値の型を推測することです。
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType<typeof add>; // type AddReturnType = number
function greet(name: string): string {
return `Hello, ${name}!`;
}
type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string
この例では、ReturnType<T>
は関数型T
を入力として受け取ります。T
が、任意の引数を受け取り値を返す関数に割り当て可能かどうかをチェックします。割り当て可能であれば、戻り値の型をR
として推測して返します。それ以外の場合はany
を返します。
2. 配列要素の型を推測する
もう1つの便利なシナリオは、配列から要素の型を抽出することです。
type ArrayElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayType = ArrayElementType<number[]>; // type NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // type NotAnArrayType = never
ここで、ArrayElementType<T>
はT
が配列型であるかどうかをチェックします。そうであれば、要素の型をU
として推測して返します。そうでない場合は、never
を返します。
infer
の高度な使用例
1. コンストラクタのパラメータを推測する
infer
を使用して、コンストラクタ関数のパラメータ型を抽出できます。
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
class Person {
constructor(public name: string, public age: number) {}
}
type PersonConstructorParams = ConstructorParameters<typeof Person>; // type PersonConstructorParams = [string, number]
class Point {
constructor(public x: number, public y: number) {}
}
type PointConstructorParams = ConstructorParameters<typeof Point>; // type PointConstructorParams = [number, number]
この場合、ConstructorParameters<T>
はコンストラクタ関数型T
を受け取ります。コンストラクタパラメータの型をP
として推測し、タプルとして返します。
2. オブジェクト型からプロパティを抽出する
infer
は、マッピングされた型と条件付き型を使用して、オブジェクト型から特定のプロパティを抽出するためにも使用できます。
type PickByType<T, K extends keyof T, U> = {
[P in K as T[P] extends U ? P : never]: T[P];
};
interface User {
id: number;
name: string;
age: number;
email: string;
isActive: boolean;
}
type StringProperties = PickByType<User, keyof User, string>; // type StringProperties = { name: string; email: string; }
type NumberProperties = PickByType<User, keyof User, number>; // type NumberProperties = { id: number; age: number; }
//An interface representing geographic coordinates.
interface GeoCoordinates {
latitude: number;
longitude: number;
altitude: number;
country: string;
city: string;
timezone: string;
}
type NumberCoordinateProperties = PickByType<GeoCoordinates, keyof GeoCoordinates, number>; // type NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }
ここで、PickByType<T, K, U>
は、値が型U
に割り当て可能なT
のプロパティ(K
内のキーを持つ)のみを含む新しい型を作成します。マッピングされた型はT
のキーを反復処理し、条件付き型は指定された型と一致しないキーをフィルタリングします。
3. Promiseの操作
Promise
の解決された型を推測できます。
type Awaited<T> = T extends Promise<infer U> ? U : T;
async function fetchData(): Promise<string> {
return 'Data from API';
}
type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // type FetchDataType = string
async function fetchNumbers(): Promise<number[]> {
return [1, 2, 3];
}
type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; //type FetchedNumbersType = number[]
Awaited<T>
型は、Promiseであると予想される型T
を受け取ります。次に、型はPromiseの解決された型U
を推測し、それを返します。T
がpromiseでない場合は、Tを返します。これは、TypeScriptの新しいバージョンの組み込みユーティリティ型です。
4. Promiseの配列の型を抽出する
Awaited
と配列型推論を組み合わせると、Promisesの配列によって解決される型を推測できます。これは、Promise.all
を扱う場合に特に役立ちます。
type PromiseArrayReturnType<T extends Promise<any>[]> = {
[K in keyof T]: Awaited<T[K]>;
};
async function getUSDRate(): Promise<number> {
return 0.0069;
}
async function getEURRate(): Promise<number> {
return 0.0064;
}
const rates = [getUSDRate(), getEURRate()];
type RatesType = PromiseArrayReturnType<typeof rates>;
// type RatesType = [number, number]
この例では、最初に、2つの非同期関数getUSDRate
とgetEURRate
を定義し、為替レートの取得をシミュレートします。次に、PromiseArrayReturnType
ユーティリティ型は、配列内の各Promise
から解決された型を抽出し、各要素が対応するPromiseの待機型であるタプル型を生成します。
さまざまなドメインにおける実践的な例
1. Eコマースアプリケーション
APIから製品の詳細を取得するEコマースアプリケーションを考えてみましょう。infer
を使用して、製品データの型を抽出できます。
interface Product {
id: number;
name: string;
price: number;
description: string;
imageUrl: string;
category: string;
rating: number;
countryOfOrigin: string;
}
async function fetchProduct(productId: number): Promise<Product> {
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: productId,
name: 'Example Product',
price: 29.99,
description: 'A sample product',
imageUrl: 'https://example.com/image.jpg',
category: 'Electronics',
rating: 4.5,
countryOfOrigin: 'Canada'
});
}, 500);
});
}
type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product
function displayProductDetails(product: ProductType) {
console.log(`Product Name: ${product.name}`);
console.log(`Price: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}
fetchProduct(123).then(displayProductDetails);
この例では、Product
インターフェースと、APIから製品の詳細を取得するfetchProduct
関数を定義します。Awaited
とReturnType
を使用して、fetchProduct
関数の戻り値の型からProduct
型を抽出し、displayProductDetails
関数を型チェックできるようにします。
2. 国際化(i18n)
ロケールに基づいて異なる文字列を返す翻訳関数があるとします。infer
を使用して、この関数の戻り値の型を型安全に抽出できます。
interface Translations {
greeting: string;
farewell: string;
welcomeMessage: (name: string) => string;
}
const enTranslations: Translations = {
greeting: 'Hello',
farewell: 'Goodbye',
welcomeMessage: (name: string) => `Welcome, ${name}!`,
};
const frTranslations: Translations = {
greeting: 'Bonjour',
farewell: 'Au revoir',
welcomeMessage: (name: string) => `Bienvenue, ${name}!`,
};
function getTranslation(locale: 'en' | 'fr'): Translations {
return locale === 'en' ? enTranslations : frTranslations;
}
type TranslationType = ReturnType<typeof getTranslation>;
function greetUser(locale: 'en' | 'fr', name: string) {
const translations = getTranslation(locale);
console.log(translations.welcomeMessage(name));
}
greetUser('fr', 'Jean'); // Output: Bienvenue, Jean!
ここで、TranslationType
はTranslations
インターフェースとして推測され、greetUser
関数が翻訳された文字列にアクセスするための正しい型情報を持つことが保証されます。
3. APIレスポンスの処理
APIを操作する場合、レスポンス構造は複雑になる可能性があります。infer
は、ネストされたAPIレスポンスから特定のデータ型を抽出するのに役立ちます。
interface ApiResponse<T> {
status: number;
data: T;
message?: string;
}
interface UserData {
id: number;
username: string;
email: string;
profile: {
firstName: string;
lastName: string;
country: string;
language: string;
}
}
async function fetchUser(userId: number): Promise<ApiResponse<UserData>> {
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => {
resolve({
status: 200,
data: {
id: userId,
username: 'johndoe',
email: 'john.doe@example.com',
profile: {
firstName: 'John',
lastName: 'Doe',
country: 'USA',
language: 'en'
}
}
});
}, 500);
});
}
type UserApiResponse = Awaited<ReturnType<typeof fetchUser>>;
type UserProfileType = UserApiResponse['data']['profile'];
function displayUserProfile(profile: UserProfileType) {
console.log(`Name: ${profile.firstName} ${profile.lastName}`);
console.log(`Country: ${profile.country}`);
}
fetchUser(123).then((response) => {
if (response.status === 200) {
displayUserProfile(response.data.profile);
}
});
この例では、ApiResponse
インターフェースとUserData
インターフェースを定義します。infer
と型インデックスを使用して、APIレスポンスからUserProfileType
を抽出し、displayUserProfile
関数が正しい型を受け取るようにします。
infer
を使用するためのベストプラクティス
- シンプルに保つ:必要な場合にのみ
infer
を使用します。使いすぎると、コードの可読性と理解度が低下する可能性があります。 - 型をドキュメント化する:条件付き型と
infer
ステートメントが何をしているかを説明するコメントを追加します。 - 型をテストする:TypeScriptの型チェックを使用して、型が期待どおりに動作していることを確認します。
- パフォーマンスを考慮する:複雑な条件付き型は、コンパイル時間に影響を与える場合があります。型の複雑さに注意してください。
- ユーティリティ型を使用する:TypeScriptは、カスタム
infer
ステートメントを必要とせずに、多くの一般的な型操作タスクを簡素化できる、いくつかの組み込みユーティリティ型(例:ReturnType
、Awaited
)を提供します。
一般的な落とし穴
- 誤った推論:場合によっては、TypeScriptが期待どおりではない型を推測することがあります。型定義と条件を再確認してください。
- 循環依存関係:
infer
を使用して再帰型を定義する場合は注意が必要です。循環依存関係やコンパイルエラーが発生する可能性があります。 - 過度に複雑な型:理解と保守が難しい、過度に複雑な条件付き型を作成することは避けてください。それらをより小さく、より管理しやすい型に分割します。
infer
の代替手段
infer
は強力なツールですが、代替アプローチがより適切な状況もあります。
- 型アサーション:場合によっては、型アサーションを使用して、推測する代わりに値の型を明示的に指定できます。ただし、型アサーションは型チェックをバイパスする可能性があるため、注意してください。
- 型ガード:型ガードは、実行時チェックに基づいて値の型を絞り込むために使用できます。これは、実行時の条件に基づいて異なる型を処理する必要がある場合に役立ちます。
- ユーティリティ型:TypeScriptは、カスタム
infer
ステートメントを必要とせずに、多くの一般的な型操作タスクを処理できる豊富なユーティリティ型を提供しています。
結論
TypeScriptのinfer
キーワードは、条件付き型と組み合わせると、高度な型操作機能を解き放ちます。これにより、複雑な型構造から特定の型を抽出し、より堅牢で保守性が高く、型安全なコードを記述できます。関数の戻り値の型を推測することから、オブジェクト型からプロパティを抽出することまで、可能性は広範囲に及びます。このガイドで概説されている原則とベストプラクティスを理解することで、infer
を最大限に活用し、TypeScriptのスキルを向上させることができます。型をドキュメント化し、徹底的にテストし、必要に応じて代替アプローチを検討することを忘れないでください。infer
をマスターすることで、本当に表現力豊かで強力なTypeScriptコードを記述できるようになり、最終的に優れたソフトウェアにつながります。