日本語

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つの非同期関数getUSDRategetEURRateを定義し、為替レートの取得をシミュレートします。次に、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関数を定義します。AwaitedReturnTypeを使用して、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!

ここで、TranslationTypeTranslationsインターフェースとして推測され、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のinferキーワードは、条件付き型と組み合わせると、高度な型操作機能を解き放ちます。これにより、複雑な型構造から特定の型を抽出し、より堅牢で保守性が高く、型安全なコードを記述できます。関数の戻り値の型を推測することから、オブジェクト型からプロパティを抽出することまで、可能性は広範囲に及びます。このガイドで概説されている原則とベストプラクティスを理解することで、inferを最大限に活用し、TypeScriptのスキルを向上させることができます。型をドキュメント化し、徹底的にテストし、必要に応じて代替アプローチを検討することを忘れないでください。inferをマスターすることで、本当に表現力豊かで強力なTypeScriptコードを記述できるようになり、最終的に優れたソフトウェアにつながります。