T-CREATOR

<div />

TypeScriptでFunction Overloadsを設計に使う 柔軟なAPIパターンと使い分け

2026年1月16日
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 コードベースを維持する鍵となります。

関連リンク

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;