Featured image of post TypeScriptジェネリック制約:型安全なAPIを構築する Featured image of post TypeScriptジェネリック制約:型安全なAPIを構築する

TypeScriptジェネリック制約:型安全なAPIを構築する

TypeScriptのジェネリック制約(extends、conditional types、mapped types、infer)を活用した型安全なAPI構築手法を解説します。

TypeScriptのジェネリクスは再利用可能で型安全なコードを可能にしますが、制約なしでは過度に寛容になります。extendsキーワードを使ったジェネリック制約により、型パラメータを特定の構造に制限し、柔軟性と安全性の両立を実現します。

extends制約

ジェネリック制約の基本はextendsキーワードです。型パラメータが要求する構造を満たすことを強制します:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

K extends keyof Tにより、keyが常にobjの有効なプロパティであることをコンパイル時に保証します。一般的なパターンとして、インターフェースへの制約(T extends { id: string })、ユニオン型のメンバー制約、プリミティブ型への制約があります。


条件付き型

条件付き型はextendsを使って型の関係性をチェックし、条件に応じて型を選択します:

type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>;      // false

実用的な例として、Promise型のアンラップがあります:

type Unwrap<T> = T extends Promise<infer R> ? R : T;
type Result = Unwrap<Promise<number>>; // number

条件付き型はユニオンに対して分配的であり、IsString<string | number>boolean(true | false)と評価されます。


マップ型とkeyof

マップ型は既存の型の全プロパティを変換します。keyofとの組み合わせで強力なオブジェクト型変換を実現します:

type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Optional<T> = { [P in keyof T]?: T[P] };

TypeScriptが提供する代表的なマップ型:

説明使用例
Partial<T>全プロパティをオプション化PATCHリクエスト
Required<T>全プロパティを必須化フォーム送信
Readonly<T>全プロパティを読み取り専用に不変設定オブジェクト
Pick<T, K>特定のキーのみ選択ビューモデル作成
Record<K, T>辞書型の作成ルックアップテーブル

TS 4.1以降ではas句によるキー再マッピングが可能です:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

inferキーワード

条件付き型内のinferキーワードは、他の型から型を抽出します。ReturnTypeやParametersなどのユーティリティ型はこの仕組みで実装されています:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

実用的な用途として、コンポーネントのProps型の抽出や、サードパーティライブラリの関数シグネチャの導出があります。


ファクトリ関数

ジェネリック制約は特定の形状に準拠したオブジェクトを生成するファクトリ関数に不可欠です:

function createInstance<T, U extends T>(
  ctor: new (...args: any[]) => T,
  data: U
): T {
  return Object.assign(new ctor(), data);
}

このパターンは依存性注入コンテナ、ORMエンティティファクトリ、APIレスポンス変換機などで広く使用されています。


型安全なAPIクライアントの構築

すべての概念を統合する実践的な例として、型安全なAPIクライアントを示します:

type ApiResponse<T> = 
  | { data: T; error: null }
  | { data: null; error: string };

type Endpoints = {
  "/users": User;
  "/posts": Post;
};

async function fetchApi<E extends keyof Endpoints>(
  endpoint: E
): Promise<ApiResponse<Endpoints[E]>> {
  const res = await fetch(endpoint);
  return res.json();
}

パフォーマンスの考慮点

深くネストされたジェネリック型はコンパイル時間に影響します。過度な再帰的条件付き型を避け、複雑なマップ型の代わりにインターフェースマージを使用し、補助型エイリアスで結果をキャッシュすることで対策できます。


ジェネリック制約の習得により、TypeScriptは単なる型システムから強力な型プログラミング言語へと進化します。これらのパターンは実行時エラーを未然に防ぎ、優れた開発者体験を提供します。