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 が原因で型制約を満たせず、コンパイルエラーになります。
発生条件と解決方法
コンパイルエラーが発生する条件と解決方法を整理します。
| # | エラーコード | 発生条件 | 解決方法 |
|---|---|---|---|
| 1 | Error TS2345 | 必須設定が不足 | すべての必須メソッドを呼び出す |
| 2 | Error TS2339 | build 前にメソッド呼び出し | build() 後に SDK メソッドを使用 |
| 3 | TypeError | 実行時の 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 | 状態管理 | 型パラメータで設定状態を追跡する |
| 2 | this 型制約 | build メソッドで完全性をチェック |
| 3 | エラーメッセージ | 具体的なエラーコードと内容を含める |
| 4 | デフォルト値 | オプション設定に適切な初期値を設定 |
| 5 | バリデーション | 設定値の妥当性を検証する |
図で理解できる要点
- ビルダーは状態を型パラメータで管理し、段階的に設定を追加する
- 必須設定が完了した状態でのみ SDK インスタンスを生成できる
- エラーハンドリングは各段階で適切に実装し、問題の診断を容易にする
この設計パターンを活用することで、開発者にとって使いやすく、保守性の高い SDK を提供できます。ぜひ、あなたのプロジェクトでも試してみてください。
関連リンク
articleTypeScript SDK 設計の定石:ビルダー × ジェネリクスで直感的かつ安全な API を作る
articleTypeScript タプル/配列操作チートシート:`Length`・`Push`・`Zip`・`Chunk`を型で書く
articleTypeScript Project References 入門:大規模 Monorepo で高速ビルドを実現する設定手順
articleTypeScript Null 安全戦略の比較検証:ts-reset vs strictNullChecks vs noUncheckedIndexedAccess
articleESM/CJS 地獄から脱出!「ERR_REQUIRE_ESM」「import 文が使えない」を TypeScript で直す
articleTypeScript 型安全なフィーチャーフラグ設計:判別可能共用体で運用事故を防ぐ
articleSolidJS コンポーネント間通信チート:Context・イベント・store の選択早見
articleWebLLM 中心のクライアントサイド RAG 設計:IndexedDB とベクトル検索の組み立て
articleShell Script の set -e が招く事故を回避:pipefail・サブシェル・条件分岐の落とし穴
articleRuby の本番運用ガイド:ログ設計・メトリクス・トレースのベストプラクティス
articleVitest テストデータ設計技術:Factory / Builder / Fixture の責務分離と再利用
articleRedis キーネーミング規約チートシート:階層・区切り・TTL ルール
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来