TypeScript で関数を書いていると、「渡す引数の型によって戻り値の型も変わる」関数を型で正確に表したい場面があります。たとえば string を渡したら string、number を渡したら number が返る、といった対応です。これを1つの関数定義で表現できるのが関数のオーバーロード(Function Overloads)です。この記事では、オーバーロードシグネチャと実装シグネチャという2種類の宣言の書き方、実装側でどう型を処理するか、そしてユニオン型などで代用できる場面との使い分けまで、初心者〜中級者向けに解説します。
目次
関数のオーバーロードとは何か
関数のオーバーロードとは、1つの関数に対して「呼び出し方(引数の型や数の組み合わせ)」を複数宣言し、それぞれに対応する戻り値の型を指定できる仕組みです。JavaScript には同名の関数を複数定義する本来の意味でのオーバーロードはありませんが、TypeScript では型のうえで複数の呼び出しパターンを表現できます。
書き方は、実際の中身を持たない「オーバーロードシグネチャ」を必要な数だけ並べ、その直後に実際の処理を書く「実装シグネチャ(実装本体)」を1つだけ書く、という形になります。呼び出す側から見えるのはオーバーロードシグネチャだけで、実装シグネチャは外部からは見えません。
基本の書き方
まずは、渡した値を配列で包んで返す簡単な関数で形を確認します。string を渡したら string[]、number を渡したら number[] を返すことを型で表しています。
// オーバーロードシグネチャ(呼び出し方の宣言。本体は書かない)
function wrap(value: string): string[];
function wrap(value: number): number[];
// 実装シグネチャ(実際の処理。すべての呼び出しを受けられる型にする)
function wrap(value: string | number): string[] | number[] {
return [value] as string[] | number[];
}
const a = wrap("hello"); // 型: string[]
const b = wrap(42); // 型: number[]
上2行がオーバーロードシグネチャで、これが呼び出す側に見える「使い方の一覧」です。3つ目の function wrap(value: string | number) が実装シグネチャで、ここに実際の処理を書きます。wrap("hello") は最初のシグネチャに一致するので戻り値は string[]、wrap(42) は2つ目に一致するので number[] と、呼び出しごとに正確な型が付きます。
実装シグネチャは外から呼び出せない
ここで重要なのが、実装シグネチャは「呼び出し可能な型」としては公開されないという点です。呼び出す側が使えるのは、上に並べたオーバーロードシグネチャだけです。そのため、実装シグネチャの引数型に合致していても、どのオーバーロードにも一致しない呼び出しはエラーになります。
function wrap(value: string): string[];
function wrap(value: number): number[];
function wrap(value: string | number): string[] | number[] {
return [value] as string[] | number[];
}
// 実装シグネチャは string | number を受けるが…
const c = wrap(true);
// エラー:boolean はどのオーバーロードにも一致しない
実装シグネチャの引数は string | number ですが、これはあくまで内部で処理を書くための型であって、呼び出し可能なシグネチャではありません。呼び出す側から見えるのは string を受ける版と number を受ける版の2つだけなので、wrap(true) はエラーになります。実装シグネチャは「すべてのオーバーロードをまとめて受けられる、広めの型」で書くのがポイントです。
引数の数が変わるパターン
オーバーロードは、引数の型だけでなく引数の数が違う呼び出しを表すのにも使えます。よくあるのが、引数の有無によって戻り値の意味が変わる関数です。次は日付を作る関数で、引数なしなら現在時刻、数値を渡したらその値から Date を作る例です。
// 引数なし、または timestamp(number)、または年月日(number×3)
function createDate(): Date;
function createDate(timestamp: number): Date;
function createDate(year: number, month: number, day: number): Date;
function createDate(
a?: number,
month?: number,
day?: number
): Date {
if (a === undefined) {
return new Date();
}
if (month === undefined) {
return new Date(a); // a を timestamp として扱う
}
// 月は 0 始まりなので -1 する
return new Date(a, month - 1, day);
}
createDate(); // OK:現在時刻
createDate(1_700_000_000); // OK:timestamp から生成
createDate(2026, 7, 22); // OK:年月日から生成
createDate(2026, 7); // エラー:この引数の組み合わせは無い
3つのオーバーロードシグネチャで「引数なし」「数値1つ」「数値3つ」という呼び出し方を宣言しています。実装シグネチャでは、それらすべてを受けられるように引数をオプション(?)にし、中で undefined かどうかを見て処理を分けます。createDate(2026, 7) のように、どのオーバーロードにも当てはまらない引数2つの呼び出しはエラーになり、想定外の使い方を型で防げます。
オーバーロードとユニオン型の使い分け
「引数によって戻り値の型を変える」以外の目的なら、オーバーロードより単純な書き方で足りることがよくあります。オーバーロードは記述が増えるぶん、本当に必要な場面かを見極めて使うのが大切です。目安を表にまとめます。
| やりたいこと | 向いている書き方 |
|---|---|
| 引数の型ごとに戻り値の型を変えたい | 関数のオーバーロード |
| 複数の型を受けるが戻り値の型は同じ | ユニオン型の引数(string | number) |
| 引数を省略できるようにしたい | オプション引数(arg?)やデフォルト値 |
| 型を呼び出しごとに厳密に対応させたい | ジェネリクス |
たとえば「string でも number でも受け取り、どちらも文字列を返す」だけなら、オーバーロードを使わずユニオン型の引数1つで十分です。
// 戻り値の型が常に string なら、オーバーロードは不要
function toLabel(value: string | number): string {
return `値は ${value} です`;
}
toLabel("A"); // 型: string
toLabel(10); // 型: string
戻り値が常に同じ型ならユニオン型のほうがシンプルで読みやすくなります。オーバーロードが力を発揮するのは、あくまで「引数のパターンに応じて戻り値の型まで変わる」ケースです。
思ったオーバーロードが選ばれないとき
オーバーロードを書いたのに期待した型にならない場合、いくつか典型的な原因があります。順に見ていきます。
シグネチャは上から順にマッチする
TypeScript はオーバーロードシグネチャを上から順に評価し、最初に一致したものを採用します。そのため、広い型のシグネチャを上に書くと、下にあるより具体的なシグネチャが選ばれなくなることがあります。狭い(具体的な)シグネチャを上に、広いシグネチャを下に並べるのが基本です。
実装シグネチャの戻り値は呼び出し側に現れない
呼び出したときの戻り値の型は、あくまで一致したオーバーロードシグネチャの戻り値です。実装シグネチャに書いた string[] | number[] のような広い型がそのまま出るわけではありません。もし呼び出し結果が意図せずユニオン型になっているなら、対応するオーバーロードシグネチャを追加できていないか、引数がどのシグネチャにも一致していない可能性があります。
実装シグネチャの型が狭すぎるとエラーになる
実装シグネチャの引数・戻り値の型は、すべてのオーバーロードシグネチャを包含できる必要があります。たとえば number を受けるオーバーロードがあるのに、実装シグネチャの引数を string だけにすると、シグネチャどうしが矛盾するというエラーになります。実装側は各オーバーロードをまとめて受けられる、少し広めの型で書くようにします。
まとめ
関数のオーバーロードは、1つの関数に対して複数の呼び出し方(引数の型や数の組み合わせ)を宣言し、それぞれに対応した戻り値の型を付けられる機能です。実際の中身を持たないオーバーロードシグネチャを具体的なものから順に並べ、その下に実装シグネチャと関数本体を1つだけ書きます。呼び出す側に見えるのはオーバーロードシグネチャだけで、実装シグネチャは外から呼び出せません。戻り値の型が変わらないならユニオン型やオプション引数で足りることも多いので、「引数のパターンで戻り値の型まで変わる」場面に絞って活用すると、型の恩恵を受けつつコードをシンプルに保てます。