T-CREATOR

TypeScript SDK 設計の定石:ビルダー × ジェネリクスで直感的かつ安全な API を作る

TypeScript SDK 設計の定石:ビルダー × ジェネリクスで直感的かつ安全な API を作る

TypeScript で SDK やライブラリを設計する際、開発者にとって使いやすく、かつ型安全な API を提供することは非常に重要です。特に、複雑な設定を必要とする SDK では、ビルダーパターンとジェネリクスを組み合わせることで、直感的で安全な API を実現できます。

本記事では、ビルダーパターンとジェネリクスを活用した TypeScript SDK の設計手法について、実践的なコード例とともに解説いたします。初心者の方にもわかりやすく、段階的に説明していきますので、ぜひ最後までお読みください。

背景

SDK 設計における重要性

近年、クラウドサービスや API を利用する際、各プラットフォームが提供する SDK を使用することが一般的になっています。優れた SDK は、開発者が迷うことなく機能を利用でき、かつ実行時エラーを最小限に抑えられる設計が求められます。

TypeScript の型システムを活用することで、コンパイル時に多くのエラーを検出でき、開発体験を大幅に向上させることが可能です。

ビルダーパターンとは

ビルダーパターンは、オブジェクトの生成プロセスを段階的に構築するデザインパターンです。複雑なオブジェクトを生成する際、コンストラクタに多数の引数を渡すのではなく、メソッドチェーンを使って必要な設定を順次追加していきます。

可読性が高く、オプション設定を柔軟に扱えるため、SDK の設計に適しています。

ジェネリクスの役割

TypeScript のジェネリクスは、型の再利用性を高め、型安全性を保ちながら柔軟なコードを書くための機能です。ビルダーパターンと組み合わせることで、設定の状態に応じて利用可能なメソッドを制限し、不正な使用を防ぐことができます。

以下の図は、ビルダーパターンとジェネリクスを組み合わせた SDK 設計の全体像を示しています。

mermaidflowchart TB
  dev["開発者"] -->|"new Builder()"| builder["SDKBuilder<br/>(初期状態)"]
  builder -->|"setConfig()"| configured["SDKBuilder<br/>(設定済み)"]
  configured -->|"setAuth()"| authed["SDKBuilder<br/>(認証済み)"]
  authed -->|"build()"| sdk["SDK インスタンス<br/>(利用可能)"]
  sdk -->|"各種メソッド"| api["API 実行"]

この図から、ビルダーが段階的に状態を遷移し、最終的に完全な SDK インスタンスを生成する流れが理解できます。

課題

従来の SDK 設計の問題点

従来の SDK 設計では、以下のような問題がありました。

コンストラクタの引数が多すぎる

必須パラメータとオプションパラメータが混在し、どの引数が必要なのか直感的にわかりにくいという課題があります。

typescript// 引数が多く、順序を間違えやすい
const sdk = new SDK(
  apiKey,
  endpoint,
  timeout,
  retryCount,
  debugMode,
  customHeaders
);
必須設定の漏れを検出できない

実行時まで必須パラメータの設定漏れに気づけず、エラーが発生してから問題に気づくケースが多発します。

typescriptconst sdk = new SDK();
// apiKey を設定し忘れている
sdk.fetchData(); // 実行時エラー: Error 401: Unauthorized

エラーコード Error 401: Unauthorized は、認証情報が不足している場合に発生する典型的なエラーです。

設定の依存関係を表現できない

ある設定が他の設定に依存している場合、その関係性を型で表現できず、不正な組み合わせを防げません。

以下の図は、従来の設計における問題点を整理したものです。

mermaidflowchart LR
  problem1["引数が多い"] -->|結果| issue1["可読性低下"]
  problem2["必須設定漏れ"] -->|結果| issue2["実行時エラー"]
  problem3["依存関係未表現"] -->|結果| issue3["不正な組み合わせ"]
  issue1 & issue2 & issue3 --> result["開発体験の悪化"]

理想的な SDK の要件

理想的な SDK は、以下の要件を満たす必要があります。

#要件説明
1型安全性コンパイル時に設定漏れや不正な使用を検出
2直感的な APIメソッドチェーンで段階的に設定を追加
3柔軟性オプション設定を自由に追加・削除可能
4拡張性新しい機能を容易に追加できる構造

解決策

ビルダーパターン × ジェネリクスの基本設計

ビルダーパターンとジェネリクスを組み合わせることで、上記の課題を解決できます。基本的なアプローチは、ビルダーの状態を型パラメータで管理し、各メソッドで状態を更新していく方法です。

状態を型で管理する

ビルダーの設定状態を型パラメータで表現し、必須設定が完了しているかをコンパイル時にチェックします。

typescript// ビルダーの状態を表す型
type BuilderState = {
  hasApiKey: boolean;
  hasEndpoint: boolean;
};

この型定義により、ビルダーがどの設定を持っているかを型レベルで追跡できます。

ジェネリクスでビルダークラスを定義

ジェネリクスを使用して、ビルダークラスに状態情報を持たせます。

typescriptclass SDKBuilder<State extends BuilderState> {
  private config: Partial<{
    apiKey: string;
    endpoint: string;
    timeout: number;
  }> = {};

  constructor() {}
}

State 型パラメータが、現在のビルダーの状態を表現しています。

設定メソッドで状態を更新

各設定メソッドは、新しい状態を持つビルダーインスタンスを返します。

typescriptsetApiKey(apiKey: string): SDKBuilder<State & { hasApiKey: true }> {
  this.config.apiKey = apiKey;
  return this as any;
}

setEndpoint(endpoint: string): SDKBuilder<State & { hasEndpoint: true }> {
  this.config.endpoint = endpoint;
  return this as any;
}

State & { hasApiKey: true } により、状態が更新されたことを型で表現しています。

build メソッドで完全性をチェック

build メソッドは、必須設定がすべて完了している場合のみ呼び出せるように制約を設けます。

typescriptbuild(
  this: SDKBuilder<{ hasApiKey: true; hasEndpoint: true }>
): SDK {
  return new SDK(this.config);
}

this パラメータに型制約を設けることで、必須設定が完了していない場合はコンパイルエラーになります。

以下の図は、ビルダーパターンとジェネリクスを組み合わせた設計の仕組みを示しています。

mermaidstateDiagram-v2
  [*] --> Initial: new SDKBuilder()
  Initial --> HasApiKey: setApiKey()
  HasApiKey --> Complete: setEndpoint()
  Initial --> HasEndpoint: setEndpoint()
  HasEndpoint --> Complete: setApiKey()
  Complete --> [*]: build()

  note right of Complete
    必須設定が揃った状態でのみ
    build() を呼び出せる
  end note

この状態遷移図から、ビルダーが段階的に設定を追加し、完全な状態になってから SDK インスタンスを生成する流れが理解できます。

型安全なオプション設定

オプション設定も型安全に扱うため、オプション専用のメソッドを用意します。

typescript// オプション設定用のメソッド
setTimeout(timeout: number): SDKBuilder<State> {
  this.config.timeout = timeout;
  return this;
}

setRetryCount(count: number): SDKBuilder<State> {
  this.config.retryCount = count;
  return this;
}

オプション設定は状態を変更しないため、同じ State 型を返します。これにより、必須設定とオプション設定を明確に区別できます。

具体例

実装例:API クライアント SDK

実際に API クライアント SDK を例に、ビルダーパターンとジェネリクスを活用した実装を見ていきましょう。

型定義とインターフェース

まず、必要な型とインターフェースを定義します。

typescript// SDK の設定を表す型
interface SDKConfig {
  apiKey: string;
  endpoint: string;
  timeout?: number;
  retryCount?: number;
  customHeaders?: Record<string, string>;
}

// ビルダーの状態を表す型
type BuilderState = {
  hasApiKey: boolean;
  hasEndpoint: boolean;
};

この型定義により、設定項目と必須条件が明確になります。

初期状態の型定義

ビルダーの初期状態を表す型を定義します。

typescript// 初期状態(何も設定されていない)
type InitialState = {
  hasApiKey: false;
  hasEndpoint: false;
};
SDKBuilder クラスの実装

ビルダークラスの基本構造を実装します。

typescriptclass SDKBuilder<
  State extends BuilderState = InitialState
> {
  private config: Partial<SDKConfig> = {};

  constructor() {}

  // メソッドチェーンのための戻り値の型に注目
  private updateState<NewState extends BuilderState>(
    newConfig: Partial<SDKConfig>
  ): SDKBuilder<NewState> {
    this.config = { ...this.config, ...newConfig };
    return this as any;
  }
}

updateState は内部ヘルパーメソッドで、設定の更新と型の変換を担当します。

必須設定メソッドの実装

API キーとエンドポイントの設定メソッドを実装します。

typescriptsetApiKey(
  apiKey: string
): SDKBuilder<State & { hasApiKey: true }> {
  return this.updateState({ apiKey });
}

setEndpoint(
  endpoint: string
): SDKBuilder<State & { hasEndpoint: true }> {
  return this.updateState({ endpoint });
}

各メソッドは、設定を追加すると同時に状態を更新した新しい型を返します。

オプション設定メソッドの実装

タイムアウトやリトライ回数などのオプション設定を追加します。

typescriptsetTimeout(timeout: number): SDKBuilder<State> {
  return this.updateState({ timeout });
}

setRetryCount(retryCount: number): SDKBuilder<State> {
  return this.updateState({ retryCount });
}

setCustomHeaders(
  headers: Record<string, string>
): SDKBuilder<State> {
  return this.updateState({ customHeaders: headers });
}

オプション設定は状態型を変更しないため、柔軟に追加・削除できます。

build メソッドの実装

最終的に SDK インスタンスを生成するメソッドを実装します。

typescriptbuild(
  this: SDKBuilder<{ hasApiKey: true; hasEndpoint: true }>
): SDK {
  // 必須パラメータは型で保証されている
  const config = this.config as Required<
    Pick<SDKConfig, 'apiKey' | 'endpoint'>
  > & Partial<Omit<SDKConfig, 'apiKey' | 'endpoint'>>;

  return new SDK(config);
}

this パラメータの型制約により、必須設定がすべて完了している場合のみ呼び出せます。

SDK クラスの実装

実際の SDK クラスを実装します。

typescriptclass SDK {
  private config: SDKConfig;

  constructor(config: SDKConfig) {
    this.config = {
      timeout: 5000,
      retryCount: 3,
      ...config,
    };
  }

  async fetchData<T>(path: string): Promise<T> {
    // API 呼び出しの実装
    const url = `${this.config.endpoint}${path}`;
    // fetch 処理
    return {} as T;
  }
}

コンストラクタでデフォルト値を設定し、API 呼び出しメソッドを提供します。

使用例

実装した SDK を実際に使用する例を見ていきましょう。

基本的な使用方法

必須設定を行ってから SDK インスタンスを生成します。

typescript// 正しい使用例
const sdk = new SDKBuilder()
  .setApiKey('your-api-key')
  .setEndpoint('https://api.example.com')
  .build();

// データ取得
const data = await sdk.fetchData('/users');

メソッドチェーンにより、直感的に設定を追加できます。

オプション設定を含む使用例

タイムアウトやカスタムヘッダーなどのオプション設定を追加する場合です。

typescriptconst sdk = new SDKBuilder()
  .setApiKey('your-api-key')
  .setEndpoint('https://api.example.com')
  .setTimeout(10000)
  .setRetryCount(5)
  .setCustomHeaders({ 'X-Custom-Header': 'value' })
  .build();

オプション設定は任意の順序で追加でき、柔軟性が高くなります。

コンパイルエラーの例

必須設定が不足している場合、コンパイル時にエラーが発生します。

typescript// エラー例 1: apiKey が未設定
const sdk1 = new SDKBuilder()
  .setEndpoint('https://api.example.com')
  .build(); // コンパイルエラー

// エラーメッセージ:
// Error TS2345: Argument of type 'this' is not assignable to parameter of type 'SDKBuilder<{ hasApiKey: true; hasEndpoint: true; }>'.

このエラーにより、実行前に設定漏れを検出できます。

typescript// エラー例 2: endpoint が未設定
const sdk2 = new SDKBuilder()
  .setApiKey('your-api-key')
  .build(); // コンパイルエラー

// エラーメッセージ:
// Error TS2345: Type '{ hasApiKey: true; hasEndpoint: false; }' does not satisfy the constraint '{ hasApiKey: true; hasEndpoint: true; }'.

hasEndpoint: false が原因で型制約を満たせず、コンパイルエラーになります。

発生条件と解決方法

コンパイルエラーが発生する条件と解決方法を整理します。

#エラーコード発生条件解決方法
1Error TS2345必須設定が不足すべての必須メソッドを呼び出す
2Error TS2339build 前にメソッド呼び出しbuild() 後に SDK メソッドを使用
3TypeError実行時の null/undefined型ガードやオプショナルチェーンを使用

応用:条件付き設定

特定の条件下でのみ利用可能な設定を実装する例です。

認証方式による分岐

API キーまたは OAuth トークンのいずれかを設定する場合の実装です。

typescripttype AuthState = {
  authType?: 'apiKey' | 'oauth';
};

class AdvancedSDKBuilder<
  State extends BuilderState & AuthState = InitialState &
    AuthState
> {
  // API キー認証を設定
  useApiKeyAuth(
    apiKey: string
  ): AdvancedSDKBuilder<State & { authType: 'apiKey' }> {
    this.config.apiKey = apiKey;
    return this as any;
  }

  // OAuth 認証を設定
  useOAuthAuth(
    token: string
  ): AdvancedSDKBuilder<State & { authType: 'oauth' }> {
    this.config.oauthToken = token;
    return this as any;
  }
}

認証方式を型パラメータで管理し、適切な設定のみを許可します。

環境別設定

開発環境と本番環境で異なる設定を適用する例です。

typescripttype Environment = 'development' | 'production';

setEnvironment(
  env: Environment
): SDKBuilder<State> {
  const endpoints = {
    development: 'https://dev-api.example.com',
    production: 'https://api.example.com',
  };

  this.config.endpoint = endpoints[env];
  this.config.debugMode = env === 'development';

  return this as any;
}

環境に応じた設定を自動的に適用することで、設定ミスを防げます。

デバッグとエラーハンドリング

SDK 使用時のエラーハンドリングについても考慮します。

設定検証の追加

build メソッド内で設定値の妥当性を検証します。

typescriptbuild(
  this: SDKBuilder<{ hasApiKey: true; hasEndpoint: true }>
): SDK {
  // バリデーション
  if (!this.config.apiKey || this.config.apiKey.trim() === '') {
    throw new Error(
      'Error: Invalid API key. API key cannot be empty.'
    );
  }

  if (!this.config.endpoint?.startsWith('https://')) {
    throw new Error(
      'Error: Invalid endpoint. Endpoint must use HTTPS protocol.'
    );
  }

  return new SDK(this.config as SDKConfig);
}

エラーメッセージに具体的なエラー内容を含めることで、問題の特定が容易になります。

API 呼び出し時のエラー処理

API 呼び出し時のエラーハンドリングを実装します。

typescriptasync fetchData<T>(path: string): Promise<T> {
  const url = `${this.config.endpoint}${path}`;

  try {
    const response = await fetch(url, {
      headers: {
        'Authorization': `Bearer ${this.config.apiKey}`,
        ...this.config.customHeaders,
      },
      timeout: this.config.timeout,
    });

    if (!response.ok) {
      throw new Error(
        `Error ${response.status}: ${response.statusText}`
      );
    }

    return await response.json();
  } catch (error) {
    // エラーログの出力
    console.error('API call failed:', error);
    throw error;
  }
}

HTTP ステータスコードを含むエラーメッセージにより、問題の診断が容易になります。

以下の図は、SDK のエラーハンドリングフローを示しています。

mermaidflowchart TD
  start["API 呼び出し"] --> validate["設定検証"]
  validate -->|"検証成功"| request["HTTP リクエスト"]
  validate -->|"検証失敗"| error1["Error: 設定エラー"]

  request --> check["レスポンス確認"]
  check -->|"200 OK"| success["データ取得成功"]
  check -->|"401"| error2["Error 401:<br/>Unauthorized"]
  check -->|"500"| error3["Error 500:<br/>Server Error"]
  check -->|"ネットワークエラー"| error4["TypeError:<br/>Network Error"]

  error1 & error2 & error3 & error4 --> handle["エラーハンドリング"]
  handle --> log["ログ出力"]
  log --> rethrow["エラーを再スロー"]

このフローにより、各段階でのエラー処理が明確になります。

テストコード例

型安全性を確認するためのテストコードも重要です。

typescriptimport { describe, it, expect } from 'vitest';

describe('SDKBuilder', () => {
  it('should build SDK with required config', () => {
    const sdk = new SDKBuilder()
      .setApiKey('test-key')
      .setEndpoint('https://api.test.com')
      .build();

    expect(sdk).toBeInstanceOf(SDK);
  });

  it('should allow optional config', () => {
    const sdk = new SDKBuilder()
      .setApiKey('test-key')
      .setEndpoint('https://api.test.com')
      .setTimeout(20000)
      .build();

    expect(sdk).toBeInstanceOf(SDK);
  });
});

テストにより、ビルダーの動作を確認し、リグレッションを防げます。

まとめ

本記事では、TypeScript SDK 設計における定石として、ビルダーパターンとジェネリクスを組み合わせた手法を解説しました。

要点の整理

ビルダーパターンとジェネリクスを活用することで、以下のメリットが得られます。

型安全性の向上 コンパイル時に設定漏れや不正な使用を検出でき、実行時エラーを大幅に削減できます。必須パラメータの設定忘れを防ぎ、開発者に安心感を提供します。

直感的な API 設計 メソッドチェーンにより、段階的に設定を追加する直感的な使用感を実現できます。コードの可読性が向上し、チーム開発でも理解しやすくなるでしょう。

柔軟性と拡張性 オプション設定を自由に追加でき、将来的な機能追加にも対応しやすい構造になります。

実装時のポイント

実装する際は、以下の点に注意してください。

#ポイント説明
1状態管理型パラメータで設定状態を追跡する
2this 型制約build メソッドで完全性をチェック
3エラーメッセージ具体的なエラーコードと内容を含める
4デフォルト値オプション設定に適切な初期値を設定
5バリデーション設定値の妥当性を検証する

図で理解できる要点

  • ビルダーは状態を型パラメータで管理し、段階的に設定を追加する
  • 必須設定が完了した状態でのみ SDK インスタンスを生成できる
  • エラーハンドリングは各段階で適切に実装し、問題の診断を容易にする

この設計パターンを活用することで、開発者にとって使いやすく、保守性の高い SDK を提供できます。ぜひ、あなたのプロジェクトでも試してみてください。

関連リンク