TypeScriptにおいて、コードの柔軟性と型安全性を両立させるために欠かせない機能の一つが「関数オーバーロード」です。

開発者が意図した通りの型推論を導き出し、利用側にとって使いやすいインターフェースを提供するためには、この機能を正しく理解する必要があります。

2026年現在のモダンなフロントエンドおよびバックエンド開発においても、複雑なロジックをシンプルに見せる手法として関数オーバーロードは頻繁に活用されています。

本記事では、関数オーバーロードの基礎から、ユニオン型との使い分け、そして実戦で役立つ応用パターンまでを詳しく解説します。

関数オーバーロードの基本概念

関数オーバーロードとは、一つの関数名に対して異なる引数の構成を持つ複数の定義を持たせる仕組みのことです。

TypeScriptでは、外部から呼び出される際の型を定義する「オーバーロードシグネチャ」と、実際の処理を記述する「実装シグネチャ」を分けて記述します。

関数オーバーロードを使用することで、引数の型に応じて戻り値の型を動的に変化させることが可能になります。

これにより、any型や広範なユニオン型に頼ることなく、厳密な型チェックを維持したまま多様な入力に対応できます。

オーバーロードの構成要素

関数オーバーロードを実装するためには、まず関数の名前、引数、戻り値の型だけを宣言するシグネチャを複数記述します。

これらは関数の中身を持たず、あくまでコンパイラに対して「このような呼び出し方が可能である」と伝える役割を果たします。

その直後に、すべてのオーバーロードシグネチャと互換性のある具体的な実装を一つだけ記述します。

実装シグネチャ自体は、外部のコードから直接呼び出すことはできないという点に注意が必要です。

TypeScript
// オーバーロードシグネチャ:文字列を受け取って数値を返す
function convert(value: string): number;
// オーバーロードシグネチャ:数値を受け取って文字列を返す
function convert(value: number): string;
// 実装シグネチャ
function convert(value: string | number): string | number {
    if (typeof value === "string") {
        return value.length;
    }
    return value.toString();
}

const result1 = convert("TypeScript"); // 型は number と推論される
const result2 = convert(2026);       // 型は string と推論される

なぜユニオン型ではなくオーバーロードを使うのか

多くの場合、関数の引数に複数の型を許容するにはユニオン型 (string | number など) を使用するだけで十分です。

しかし、ユニオン型だけでは「引数がこの型なら、戻り値はこの型になる」という対応関係を定義することができません。

例えば、引数が string なら戻り値も必ず string になり、引数が number なら戻り値も必ず number になるような関数を考えてみましょう。

ユニオン型で定義すると、戻り値の型も string | number になってしまい、呼び出し側で再度型ガードが必要になります。

TypeScript
// ユニオン型のみの場合
function identity(value: string | number): string | number {
    return value;
}

const val = identity("hello"); // val の型は string | number になってしまう

このようなケースにおいて、関数オーバーロードを使用すれば、呼び出し側の型を確定させることができます。

呼び出し側の利便性と安全性を最大化することが、関数オーバーロードの主な目的です。

実践的な書き方:引数の数によるオーバーロード

型だけでなく、引数の個数によって動作を変えたい場合にも関数オーバーロードは非常に有効です。

例えば、座標を指定してオブジェクトを生成する関数において、1つの数値で全座標を指定する場合と、個別に X と Y を指定する場合をサポートする例を見てみましょう。

TypeScript
interface Point {
    x: number;
    y: number;
}

// 1つの引数で x, y 両方を設定
function createPoint(all: number): Point;
// 2つの引数で個別に設定
function createPoint(x: number, y: number): Point;
// 実装
function createPoint(a: number, b?: number): Point {
    return {
        x: a,
        y: b ?? a
    };
}

const p1 = createPoint(10);     // { x: 10, y: 10 }
const p2 = createPoint(10, 20); // { x: 10, y: 20 }

このように記述することで、利用者はエディタの補完機能を通じて「2通りの使い方がある」ことを明確に認識できます。

実装シグネチャでは、任意引数 (?) を活用してすべてのパターンを網羅するように設計します。

関数のオーバーロードを設計する際の重要ルール

関数オーバーロードを適切に機能させるためには、いくつかの厳守すべきルールがあります。

1. 最も具体的なシグネチャを先に記述する

TypeScriptのコンパイラは、記述された順番にオーバーロードシグネチャをチェックし、最初に一致したものを採用します。

より一般的、あるいは広範な型を持つシグネチャを先に書いてしまうと、後続の具体的なシグネチャが無視される可能性があります。

常に「限定的な型」から「汎用的な型」の順で記述することを心がけてください。

2. 実装シグネチャはすべてのシグネチャと互換性を持つこと

実装シグネチャの引数や戻り値の型は、定義したすべてのオーバーロードを包含していなければなりません。

もし一つでも互換性のないシグネチャが含まれていると、コンパイルエラーが発生します。

このため、実装シグネチャではユニオン型や unknown 型を駆使して柔軟な型定義を行うのが一般的です。

3. 不要なオーバーロードを避ける

もしユニオン型やオプショナル引数だけでシンプルに解決できるのであれば、関数オーバーロードを導入すべきではありません。

オーバーロードは定義が長くなりやすく、メンテナンスコストを増加させる側面もあります。

コードの可読性を損なわない範囲で、真に型安全性が向上する場合にのみ採用を検討しましょう。

高度な実戦例:APIレスポンスのパース

より実戦的なシナリオとして、APIからのデータを取得し、フォーマット指定に応じて戻り値の型を変更する関数を考えてみます。

「生のデータ」と「整形済みのデータ」を使い分けたい場合に、オーバーロードは真価を発揮します。

TypeScript
type User = { id: number; name: string };
type UserJson = { id: number; name: string; age: number };

function getUser(id: number, format: "raw"): UserJson;
function getUser(id: number, format: "simple"): User;
function getUser(id: number, format: "raw" | "simple"): User | UserJson {
    const rawData = { id, name: "Taro", age: 30 }; // ダミーデータ

    if (format === "raw") {
        return rawData;
    }
    return { id: rawData.id, name: rawData.name };
}

const rawUser = getUser(1, "raw");       // 型は UserJson
const simpleUser = getUser(1, "simple"); // 型は User

この例では、文字列のリテラル型をフラグとして使い、戻り値の型を切り分けています。

これにより、呼び出し側は型アサーション (as User) を使う必要がなくなり、実行時のバグを未然に防ぐことができます。

アロー関数におけるオーバーロードの注意点

これまで紹介した function キーワードによる宣言とは異なり、アロー関数では直接的なオーバーロード構文がサポートされていません。

アロー関数でオーバーロードを実現するには、インターフェースや型エイリアスを用いて関数の型を事前に定義する必要があります。

TypeScript
interface OverloadedFn {
    (val: string): string;
    (val: number): number;
}

const double: OverloadedFn = (val: any) => {
    return typeof val === "string" ? val + val : val * 2;
};

console.log(double("A")); // "AA"
console.log(double(10));  // 20

この手法は、Reactのコンポーネントや高階関数を定義する際に、型定義を外部へ切り出す場合に便利です。

ジェネリクスとの組み合わせによる柔軟な設計

関数オーバーロードとジェネリクスを組み合わせることで、さらに強力な型システムを構築できます。

例えば、配列を受け取ってその最初の要素を返す関数において、空の配列の場合は undefined を返し、要素がある場合はその要素の型を正確に返す定義が可能です。

TypeScript
function first<T>(arr: readonly T[]): T;
function first<T>(arr: []): undefined;
function first<T>(arr: any): any {
    return arr[0];
}

const item = first([1, 2, 3]); // number 型
const empty = first([]);      // undefined 型

このようにジェネリクスを併用することで、特定の構造を持つデータに対して柔軟に、かつ厳密に型を適用することができます。

まとめ

TypeScriptの関数オーバーロードは、一つの関数に複数の顔を持たせ、それらを型安全に管理するための優れたツールです。

単に引数の型を増やすだけでなく、引数と戻り値の「論理的な関係性」をコンパイラに伝えることができる点が最大のメリットです。

実装シグネチャでの適切な型ガードを組み合わせることで、堅牢なプログラムを作成することが可能になります。

しかし、過度なオーバーロードはコードを複雑にするため、ユニオン型やジェネリクスで簡潔に記述できないかを常に検討することも重要です。

今回解説したルールとパターンを参考に、プロジェクトにとって最適な関数のインターフェースを設計してみてください。

関数オーバーロードをマスターすることは、開発者体験 (DX) の向上だけでなく、チーム全体のコード品質を高めることに直結するはずです。