TypeScriptでFunction Overloadsを設計に使う 柔軟なAPIパターンと使い分け
TypeScript で「引数のパターンによって戻り値の型を変えたい」「同じ関数名で複数の使い方をサポートしたい」と考えたことはありませんか。本記事では、Function Overloads(関数オーバーロード)を設計に活用する方法と、ジェネリクスや Union 型といった代替案との使い分けを解説します。型推論を最大限に活かした API 設計を目指す方に向けて、実務で役立つ判断基準をお伝えします。
Function Overloads と代替手法の比較
| 手法 | 型安全性 | 可読性 | 保守性 | 適用場面 |
|---|---|---|---|---|
| Function Overloads | ◎ 引数パターンごとに厳密な型 | △ シグネチャが増えると複雑 | △ 変更時に複数箇所修正 | 引数と戻り値の対応が明確なとき |
| ジェネリクス | ◎ 型パラメータで柔軟に推論 | ○ 単一シグネチャで表現 | ◎ 型パラメータの変更のみ | 型の関係性を抽象化したいとき |
| Union 型 + 型ガード | ○ ガードで絞り込み可能 | ○ 呼び出し側は単純 | ○ ガード関数の管理が必要 | 複数型を受け入れる汎用処理 |
| 型エイリアス + Conditional Types | ◎ 条件に応じた型変換 | △ 型定義が複雑になりやすい | △ 型定義の理解が必要 | 高度な型変換が必要なとき |
それぞれの詳細と判断基準は後述します。
検証環境
- OS: macOS Sonoma 14.7
- Node.js: 22.13.0
- TypeScript: 5.7.3
- 主要パッケージ:
- @types/node: 22.10.7
- 検証日: 2026 年 01 月 16 日
Function Overloads が必要になる背景
この章では、なぜ Function Overloads という機能が TypeScript に存在するのか、その技術的・実務的な背景を整理します。
JavaScript の動的型付けと TypeScript の静的型付けの橋渡し
JavaScript では、同じ関数が異なる型の引数を受け取り、異なる型の値を返すパターンが頻繁に使われます。たとえば、DOM API の querySelector は引数のセレクタ文字列によって返す要素の型が異なります。
typescript// DOM API の例:セレクタによって戻り値の型が変わる
const div = document.querySelector("div"); // HTMLDivElement | null
const input = document.querySelector("input"); // HTMLInputElement | null
const custom = document.querySelector(".my-class"); // Element | null
TypeScript はこの柔軟性を型安全に表現するために Function Overloads を提供しています。
実務で発生する「引数と戻り値の対応」問題
実際に試したところ、API クライアントやユーティリティ関数を設計する際に「特定の引数を渡したら特定の型が返る」という対応関係を表現したい場面が多発しました。
mermaidflowchart LR
A["引数パターン A"] --> R1["戻り値型 A"]
B["引数パターン B"] --> R2["戻り値型 B"]
C["引数パターン C"] --> R3["戻り値型 C"]
上図は、引数のパターンによって戻り値の型が分岐する概念を示しています。Function Overloads はこの対応関係をコンパイル時に保証します。
Function Overloads を使わない場合に発生する課題
この章では、Function Overloads を使わずに同様の機能を実装しようとした場合に発生する問題を整理します。
戻り値の型が Union 型に広がる問題
Function Overloads を使わずに複数の引数パターンを受け入れると、戻り値の型が Union 型に広がり、呼び出し側で型ガードが必要になります。
typescript// オーバーロードなしの実装
function getData(id: string): User | null;
function getData(ids: string[]): User[];
function getData(idOrIds: string | string[]): User | User[] | null {
if (Array.isArray(idOrIds)) {
return idOrIds.map((id) => ({ id, name: `User ${id}` }));
}
return idOrIds ? { id: idOrIds, name: `User ${idOrIds}` } : null;
}
// 呼び出し側で型が広がる
const result = getData("123"); // User | User[] | null になってしまう
検証の結果、上記のコードでは result の型が User | User[] | null となり、実際には User | null であるはずなのに、呼び出し側で不要な型チェックが必要になりました。
型推論が効かずエディタ補完が弱くなる問題
戻り値の型が広がると、IDE のオートコンプリートや型チェックの精度が低下します。業務で問題になったのは、チームメンバーが関数の使い方を誤り、本来不要な型ガードを追加してコードが冗長になったケースです。
つまずきやすい点:Union 型が広がると、TypeScript の型推論が「可能性のある型すべて」を考慮するため、呼び出し側のコードが複雑になります。
Function Overloads と代替案の比較と判断基準
この章では、Function Overloads を採用すべき場面と、ジェネリクスや Union 型といった代替案を選ぶべき場面を整理します。
Function Overloads を採用した設計
引数のパターンと戻り値の型が明確に対応している場合、Function Overloads が最適です。以下は動作確認済みのコード例です。
typescriptinterface User {
id: string;
name: string;
}
// オーバーロードシグネチャ(型定義)
function getUser(id: string): User | null;
function getUser(ids: string[]): User[];
// 実装シグネチャ(実際の処理)
function getUser(idOrIds: string | string[]): User | User[] | null {
if (Array.isArray(idOrIds)) {
return idOrIds.map((id) => ({ id, name: `User ${id}` }));
}
return idOrIds ? { id: idOrIds, name: `User ${idOrIds}` } : null;
}
// 呼び出し側:型が正しく推論される
const singleUser = getUser("123"); // User | null
const multipleUsers = getUser(["1", "2", "3"]); // User[]
つまずきやすい点:オーバーロードシグネチャは上から順に評価されます。より具体的なシグネチャを先に書き、汎用的なシグネチャを後に書く必要があります。
ジェネリクスを採用した設計
型パラメータで関係性を抽象化したい場合、ジェネリクスが適しています。採用しなかった理由として、引数と戻り値の対応が 1 対 1 でない場合はジェネリクスのほうが保守しやすいことが挙げられます。
typescript// ジェネリクスによる設計
interface Repository<T> {
find(id: string): Promise<T | null>;
findMany(ids: string[]): Promise<T[]>;
}
function createRepository<T>(
fetcher: (id: string) => Promise<T>,
): Repository<T> {
return {
async find(id: string): Promise<T | null> {
try {
return await fetcher(id);
} catch {
return null;
}
},
async findMany(ids: string[]): Promise<T[]> {
const results = await Promise.all(ids.map((id) => fetcher(id)));
return results.filter((r): r is T => r !== null);
},
};
}
Union 型 + 型ガードを採用した設計
複数の型を受け入れる汎用的な処理では、Union 型と型ガードの組み合わせが有効です。
typescript// 型ガード関数
function isString(value: unknown): value is string {
return typeof value === "string";
}
function isNumber(value: unknown): value is number {
return typeof value === "number";
}
// Union 型を受け入れる関数
function format(value: string | number | Date): string {
if (isString(value)) {
return value.trim();
}
if (isNumber(value)) {
return value.toLocaleString();
}
return value.toISOString();
}
mermaidflowchart TD
Input["入力: string | number | Date"]
Input --> Check1{"typeof === 'string'?"}
Check1 -->|Yes| S["trim() で整形"]
Check1 -->|No| Check2{"typeof === 'number'?"}
Check2 -->|Yes| N["toLocaleString() で整形"]
Check2 -->|No| D["toISOString() で整形"]
上図は、Union 型を型ガードで絞り込む処理の流れを示しています。
Function Overloads の具体的な実装パターン
この章では、実務で使える Function Overloads の具体的な実装パターンを紹介します。
パターン 1:オプション引数による戻り値型の分岐
設定オブジェクトの有無で戻り値の型を変える実装パターンです。
typescriptinterface DetailedResult {
data: string;
metadata: {
timestamp: Date;
source: string;
};
}
// オプションの有無で戻り値型を分岐
function fetchData(url: string): Promise<string>;
function fetchData(
url: string,
options: { detailed: true },
): Promise<DetailedResult>;
function fetchData(
url: string,
options?: { detailed?: boolean },
): Promise<string | DetailedResult> {
if (options?.detailed) {
return Promise.resolve({
data: `Response from ${url}`,
metadata: {
timestamp: new Date(),
source: url,
},
});
}
return Promise.resolve(`Response from ${url}`);
}
// 使用例
async function example() {
const simple = await fetchData("/api/users"); // string
const detailed = await fetchData("/api/users", { detailed: true }); // DetailedResult
}
パターン 2:リテラル型による戻り値型の分岐
リテラル型を引数に使うことで、より厳密な型推論を実現できます。
typescriptinterface TextConfig {
format: "plain";
content: string;
}
interface JsonConfig {
format: "json";
data: Record<string, unknown>;
}
interface BinaryConfig {
format: "binary";
buffer: ArrayBuffer;
}
// リテラル型で戻り値を分岐
function createConfig(format: "plain", content: string): TextConfig;
function createConfig(
format: "json",
data: Record<string, unknown>,
): JsonConfig;
function createConfig(format: "binary", buffer: ArrayBuffer): BinaryConfig;
function createConfig(
format: "plain" | "json" | "binary",
payload: string | Record<string, unknown> | ArrayBuffer,
): TextConfig | JsonConfig | BinaryConfig {
switch (format) {
case "plain":
return { format, content: payload as string };
case "json":
return { format, data: payload as Record<string, unknown> };
case "binary":
return { format, buffer: payload as ArrayBuffer };
}
}
// 使用例:型が正しく推論される
const text = createConfig("plain", "Hello"); // TextConfig
const json = createConfig("json", { key: "value" }); // JsonConfig
パターン 3:ジェネリクスとの組み合わせ
ジェネリクスと Function Overloads を組み合わせることで、より柔軟な型推論を実現できます。
typescriptinterface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
}
// ジェネリクスとオーバーロードの組み合わせ
function query<T>(sql: string): Promise<T[]>;
function query<T>(
sql: string,
options: { paginated: true; page: number; pageSize: number },
): Promise<PaginatedResponse<T>>;
function query<T>(
sql: string,
options?: { paginated?: boolean; page?: number; pageSize?: number },
): Promise<T[] | PaginatedResponse<T>> {
if (options?.paginated) {
return Promise.resolve({
items: [] as T[],
total: 0,
page: options.page ?? 1,
pageSize: options.pageSize ?? 20,
});
}
return Promise.resolve([] as T[]);
}
// 使用例
interface Product {
id: string;
name: string;
price: number;
}
async function fetchProducts() {
const all = await query<Product>("SELECT * FROM products"); // Product[]
const paginated = await query<Product>("SELECT * FROM products", {
paginated: true,
page: 1,
pageSize: 10,
}); // PaginatedResponse<Product>
}
つまずきやすい点:ジェネリクスとオーバーロードを組み合わせる場合、型パラメータの推論順序に注意が必要です。オーバーロードシグネチャごとに型パラメータの制約を明示すると、意図しない型推論を防げます。
オーバーロード設計時の注意点と失敗談
この章では、実務で遭遇した失敗パターンとその回避策を紹介します。
失敗パターン 1:シグネチャの順序ミス
実際に試したところ、以下のようにシグネチャの順序を間違えると、意図した型推論が行われませんでした。
typescript// ❌ 問題のあるシグネチャ順序
function badProcess(data: unknown): string;
function badProcess(data: string): string; // 到達不可能
function badProcess(data: number): number; // 到達不可能
function badProcess(data: unknown): string | number {
return String(data);
}
// ✅ 正しいシグネチャ順序
function goodProcess(data: string): string;
function goodProcess(data: number): number;
function goodProcess(data: unknown): string;
function goodProcess(data: unknown): string | number {
if (typeof data === "string") return data.toUpperCase();
if (typeof data === "number") return data * 2;
return String(data);
}
失敗パターン 2:オーバーロードの過剰使用
業務で問題になったケースとして、オーバーロードを多用しすぎてシグネチャが 10 個以上になり、保守が困難になった事例があります。このような場合は、型エイリアスや Conditional Types を使った設計に切り替えることを検討します。
typescript// ❌ オーバーロードが多すぎる例(省略)
// ✅ 型エイリアスで整理した例
type FormatType = "text" | "json" | "xml" | "binary";
type FormatResult<T extends FormatType> = T extends "text"
? string
: T extends "json"
? Record<string, unknown>
: T extends "xml"
? Document
: ArrayBuffer;
function format<T extends FormatType>(type: T, data: unknown): FormatResult<T> {
// 実装
return data as FormatResult<T>;
}
Function Overloads と代替案の詳細比較
この章では、判断に迷った際の具体的な選択基準を提供します。
| 観点 | Function Overloads | ジェネリクス | Union 型 + 型ガード | Conditional Types |
|---|---|---|---|---|
| 型安全性 | ◎ 引数パターンごとに厳密 | ◎ 型パラメータで柔軟 | ○ ガードで絞り込み | ◎ 条件に応じた型変換 |
| 可読性 | △ シグネチャ増加で低下 | ○ 単一シグネチャ | ○ 呼び出し側は単純 | △ 型定義が複雑 |
| 保守性 | △ 複数箇所の修正が必要 | ◎ 型パラメータの変更のみ | ○ ガード関数の管理 | △ 型定義の理解が必要 |
| IDE 補完 | ◎ パターン別に補完 | ◎ 型パラメータで補完 | ○ Union 型の候補表示 | ○ 解決後の型を表示 |
| 学習コスト | 低 | 中 | 低 | 高 |
| 適用場面 | 引数と戻り値が 1 対 1 | 型の関係性を抽象化 | 汎用的な複数型処理 | 高度な型変換 |
選択フローチャート
mermaidflowchart TD
Start["関数の設計を検討"]
Q1{"引数パターンと戻り値が<br/>1 対 1 で対応?"}
Q2{"パターン数は<br/>5 個以下?"}
Q3{"型の関係性を<br/>抽象化したい?"}
Q4{"複数型を受け入れる<br/>汎用処理?"}
Start --> Q1
Q1 -->|Yes| Q2
Q1 -->|No| Q3
Q2 -->|Yes| Overloads["Function Overloads"]
Q2 -->|No| Conditional["Conditional Types"]
Q3 -->|Yes| Generics["ジェネリクス"]
Q3 -->|No| Q4
Q4 -->|Yes| Union["Union 型 + 型ガード"]
Q4 -->|No| Generics
上図は、設計手法を選択する際の判断フローを示しています。
まとめ
Function Overloads は、引数のパターンと戻り値の型が明確に対応している場合に、型推論を最大限に活かせる強力な機能です。ただし、シグネチャが増えすぎると保守性が低下するため、5 個程度を目安に、それ以上になる場合はジェネリクスや Conditional Types への切り替えを検討します。
本記事で紹介した判断基準を参考に、プロジェクトの要件に合った設計手法を選択してください。型安全性と保守性のバランスを取ることが、長期的に安定した TypeScript コードベースを維持する鍵となります。
関連リンク
著書
article2026年1月18日TypeScriptで非同期処理を型安全に書く使い方 Promiseとasync awaitの型定義を整理
article2026年1月16日TypeScriptの高度な型操作を使い方で理解する keyof typeof inferを実例で整理
article2026年1月16日TypeScriptでFunction Overloadsを設計に使う 柔軟なAPIパターンと使い分け
article2026年1月13日TypeScriptでHigher Kinded Typesを模倣する設計 ジェネリクスで関数型パターンを整理
article2026年1月8日TypeScriptでAPIクライアント自動生成をセットアップする手順 OpenAPIとgRPC導入の要点
article2025年12月28日TypeScriptとRxJSのユースケース ジェネリクスで型安全なリアクティブ設計をまとめる
article2026年1月19日TypeScriptでクラスを設計する 継承 抽象クラス ミックスインの使い分けと判断基準
article2026年1月19日TypeScriptのenumとunion型を比較・検証する どちらを選ぶべきか判断基準を整理
article2026年1月18日TypeScriptで非同期処理を型安全に書く使い方 Promiseとasync awaitの型定義を整理
article2026年1月18日TypeScriptの型注釈を使い方で整える 型エラーを減らす推論の活かし方と手順
article2026年1月17日TypeScriptでBrand Typesを設計に使う 値の取り違えを型で防ぐ実践
article2026年1月17日TypeScriptのnamespaceとmoduleを設計で使い分ける 正しい使い方と判断指針
article2026年1月19日TypeScriptでクラスを設計する 継承 抽象クラス ミックスインの使い分けと判断基準
article2026年1月19日TypeScriptのenumとunion型を比較・検証する どちらを選ぶべきか判断基準を整理
article2026年1月18日TypeScriptで非同期処理を型安全に書く使い方 Promiseとasync awaitの型定義を整理
article2026年1月18日TypeScriptの型注釈を使い方で整える 型エラーを減らす推論の活かし方と手順
article2026年1月17日TypeScriptでBrand Typesを設計に使う 値の取り違えを型で防ぐ実践
article2026年1月17日TypeScriptのnamespaceとmoduleを設計で使い分ける 正しい使い方と判断指針
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 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
