- TypeScript
TypeScript オーバーロードの書き方|引数の型で戻り値を変える3つの方法
TypeScript のオーバーロードとは、1つの関数に「引数と戻り値の型の組み合わせ(シグネチャ)」を複数並べて宣言し、呼び出し側が渡した引数に合うシグネチャの戻り値の型を受け取れるようにする書き方です。 実装は1つだけで、宣言を上に並べ、最後に本体つきの実装を書きます。
「引数の型で戻り値の型を変えたい」ときの方法は、オーバーロードのほかにユニオン型とジェネリクス+条件型があります。この記事では同じ関数を3通りで書き、呼び出し側で得られる型・実装側で必要な as・ユニオン型の値を渡せるかを比べます。あわせて、関数宣言・メソッド・コンストラクタ・アロー関数それぞれの書き方と、TS2769・TS2394 などのエラー全文を載せます。
確認環境:Ubuntu 24.04、TypeScript 7.0.2(npx tsc --version の表示は Version 7.0.2)、Node.js 22.22.2。TS2769 の表示だけ TypeScript 5.9.3 とも比べています。エラー文は npx tsc -p . の出力をそのまま貼っています。
3つの方法の早見表
「string を渡すと string、number を渡すと number を返す」関数 double を3通りで書いた結果です。
| 比べる点 | オーバーロード | ユニオン型 | ジェネリクス+条件型 |
|---|---|---|---|
double("ab") の型 |
string | string または number | string |
double(21) の型 |
number | string または number | number |
| 実装側の as | 不要 | 不要 | 必要(なしだと TS2322) |
| string または number の値を渡す | TS2769 でエラー | 通る | 通る(戻り値は string または number) |
| 引数の個数が変わる組み合わせ | 書ける | 省略可能な引数で書ける | 書きにくい |
| コールバックに関数ごと渡す | 最後のシグネチャが使われる | そのまま渡せる | そのまま渡せる |
| 向いている場面 | 組み合わせが2〜4個で、個数や型がばらばら | 戻り値の型が引数によらない | 入力の型をそのまま戻り値に反映したい |
戻り値の型が引数で変わらないならユニオン型、変わるならオーバーロードかジェネリクス、という順で考えると選びやすくなります。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript オーバーロードの書き方|引数の型で戻り値を変える3つの方法」
同じ関数を3通りで書く
以下のコードは、次の tsconfig.json を置いたフォルダで型チェックしています。実行は node ファイル名.ts です(Node.js 22.22.2 は型注釈を取り除いて .ts をそのまま実行できます)。
{
"compilerOptions": {
"strict": true,
"target": "es2022",
"module": "nodenext",
"moduleDetection": "force",
"noEmit": true
}
}
// three-ways.ts
// (1) オーバーロード
function doubleA(value: string): string;
function doubleA(value: number): number;
function doubleA(value: string | number): string | number {
return typeof value === "string" ? value + value : value * 2;
}
// (2) ユニオン型
function doubleB(value: string | number): string | number {
return typeof value === "string" ? value + value : value * 2;
}
// (3) ジェネリクス+条件型
type Doubled<T> = T extends string ? string : number;
function doubleC<T extends string | number>(value: T): Doubled<T> {
return (typeof value === "string" ? value + value : (value as number) * 2) as Doubled<T>;
}
const a1 = doubleA("ab");
const a2 = doubleA(21);
const b1 = doubleB("ab");
const b2 = doubleB(21);
const c1 = doubleC("ab");
const c2 = doubleC(21);
console.log(a1, a2, b1, b2, c1, c2);
$ node three-ways.ts
abab 42 abab 42 abab 42
実行結果は3つとも同じです。違いは型に出ます。npx tsc three-ways.ts --ignoreConfig --strict --target es2022 --declaration --emitDeclarationOnly --outDir dts で型定義ファイルを出力すると、各変数の型は次のとおりでした。
declare const a1: string;
declare const a2: number;
declare const b1: string | number;
declare const b2: string | number;
declare const c1: string;
declare const c2: number;
ユニオン型(doubleB)は、何を渡しても戻り値が string | number になります。 呼び出し側で b1.toUpperCase() と書くには、もう一度 typeof で絞り込む必要があります。オーバーロードとジェネリクス+条件型は、渡した型に対応する戻り値の型が得られます。
ジェネリクス+条件型は実装側に as が必要
(3) の実装から as を外すと、次のエラーになります。
// noas.ts
type Doubled<T> = T extends string ? string : number;
function doubleC<T extends string | number>(value: T): Doubled<T> {
if (typeof value === "string") {
return value + value;
}
return value * 2;
}
noas.ts(4,5): error TS2322: Type 'string' is not assignable to type 'Doubled<T>'.
noas.ts(6,3): error TS2322: Type 'number' is not assignable to type 'Doubled<T>'.
noas.ts(6,10): error TS2362: The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
関数の中では T がまだ決まっていないため、Doubled<T> が string なのか number なのかをコンパイラが決められません。そのため実装側は as で型を伝える形になります。オーバーロードは、実装の戻り値を string | number と書くだけで済み、as は不要です。 どちらの方法でも「string のときに string を返す」という対応そのものは実装を書く人が守ります。
ユニオン型の値を渡すと、オーバーロードだけ通らない
呼び出し側の値が string | number 型のとき、3つの方法で結果が分かれます。
// union-arg.ts
function doubleA(value: string): string;
function doubleA(value: number): number;
function doubleA(value: string | number): string | number {
return typeof value === "string" ? value + value : value * 2;
}
function doubleB(value: string | number): string | number {
return typeof value === "string" ? value + value : value * 2;
}
type Doubled<T> = T extends string ? string : number;
function doubleC<T extends string | number>(value: T): Doubled<T> {
return (typeof value === "string" ? value + value : (value as number) * 2) as Doubled<T>;
}
declare const input: string | number;
const a = doubleA(input);
const b = doubleB(input);
const c = doubleC(input);
union-arg.ts(16,19): error TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type 'string | number' is not assignable to parameter of type 'number'.
Type 'string' is not assignable to type 'number'.
エラーになったのは doubleA だけです。オーバーロードはシグネチャを1つずつ試し、引数がまるごと合うものを探すため、string | number は「string だけ」にも「number だけ」にも合いません。doubleB と doubleC は通り、型定義ファイルで確かめると b も c も string | number でした。
オーバーロードのまま通したいときは、ユニオン型を受けるシグネチャを最後に足します。
// union-fix.ts
function doubleA(value: string): string;
function doubleA(value: number): number;
function doubleA(value: string | number): string | number;
function doubleA(value: string | number): string | number {
return typeof value === "string" ? value + value : value * 2;
}
declare const input: string | number;
const a = doubleA(input); // string | number
const s = doubleA("ab"); // string
const n = doubleA(21); // number
このファイルはエラーなしで通り、コメントの型は型定義ファイルの出力と一致しています。3行目と4行目は同じに見えますが、3行目は「外から呼べる宣言」、4行目は「実装」で、役割が違います。
書き方:関数宣言・メソッド・コンストラクタ・アロー関数
オーバーロードは書く場所ごとに形が少し違います。5つの書き方を1ファイルにまとめました。
// forms.ts
// 1. 関数宣言
function format(value: Date): string;
function format(value: number, digits: number): string;
function format(value: Date | number, digits?: number): string {
return value instanceof Date ? value.toISOString().slice(0, 10) : value.toFixed(digits);
}
// 2. クラスのメソッド
class Stock {
private items = new Map<string, number>([["pen", 12], ["note", 3]]);
get(name: string): number | undefined;
get(names: string[]): (number | undefined)[];
get(arg: string | string[]): number | undefined | (number | undefined)[] {
return Array.isArray(arg) ? arg.map((n) => this.items.get(n)) : this.items.get(arg);
}
}
// 3. コンストラクタ
class Point {
x: number;
y: number;
constructor(x: number, y: number);
constructor(text: string);
constructor(a: number | string, b?: number) {
if (typeof a === "string") {
const [x, y] = a.split(",").map(Number);
this.x = x ?? 0;
this.y = y ?? 0;
} else {
this.x = a;
this.y = b ?? 0;
}
}
}
// 4. アロー関数(呼び出しシグネチャを並べた型を付ける)
type Pad = {
(value: string): string;
(value: number, width: number): string;
};
const pad: Pad = (value: string | number, width?: number): string =>
typeof value === "string" ? value.trim() : String(value).padStart(width ?? 0, "0");
// 5. interface に書く(オブジェクトのメソッド)
interface Formatter {
format(value: Date): string;
format(value: number, digits: number): string;
}
const formatter: Formatter = { format };
console.log(format(new Date("2026-10-05T00:00:00Z")), format(3.14159, 2));
const stock = new Stock();
console.log(stock.get("pen"), stock.get(["pen", "note", "ink"]));
console.log(new Point(1, 2), new Point("3,4"));
console.log(pad(" ab "), pad(7, 3));
console.log(formatter.format(1.5, 0));
$ node forms.ts
2026-10-05 3.14
12 [ 12, 3, undefined ]
Point { x: 1, y: 2 } Point { x: 3, y: 4 }
ab 007
2
| 書く場所 | 宣言の並べ方 | 実装 |
|---|---|---|
| 関数宣言 | function 名前(...): 型; を並べる |
直後に本体つきの function を1つ |
| クラスのメソッド | 名前(...): 型; を並べる |
直後に本体つきのメソッドを1つ |
| コンストラクタ | constructor(...); を並べる(戻り値の型は書かない) |
直後に本体つきの constructor を1つ |
| アロー関数 | 呼び出しシグネチャを並べた型(type か interface)を作る | その型を付けた変数にアロー関数を代入 |
| interface のメソッド | 同じ名前のメソッドを並べる | 型に合う関数を代入 |
アロー関数で戻り値の型が分かれるとき
アロー関数には「宣言を並べて最後に実装」という構文がないため、型を先に作って代入します。上の Pad はどちらのシグネチャも string を返すのでそのまま通りましたが、シグネチャごとに戻り値の型が違うと、代入の時点でエラーになります。
// arrow-err.ts
type Double = {
(value: string): string;
(value: number): number;
};
const double: Double = (value: string | number) =>
typeof value === "string" ? value + value : value * 2;
arrow-err.ts(5,7): error TS2322: Type '(value: string | number) => string | number' is not assignable to type 'Double'.
Type 'string | number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.
アロー関数の戻り値 string | number が、1つめのシグネチャの string に合わないと判定されています。アロー関数の戻り値の型を any と書くと通ります。
// arrow-fix.ts
type Double = {
(value: string): string;
(value: number): number;
};
// 戻り値の型を any にして、型の対応は Double 側に任せる
const double: Double = (value: string | number): any =>
typeof value === "string" ? value + value : value * 2;
const s = double("ab");
const n = double(21);
console.log(s, n);
$ node arrow-fix.ts
abab 42
型定義ファイルで確かめると s は string、n は number でした。戻り値の型が分かれるオーバーロードは、関数宣言で書くほうが any を使わずに済みます。
TS2769「No overload matches this call」の読み方
どのシグネチャにも合わない呼び出しをすると TS2769 になります。
// nomatch.ts
function doubleA(value: string): string;
function doubleA(value: number): number;
function doubleA(value: string | number): string | number {
return typeof value === "string" ? value + value : value * 2;
}
doubleA(true);
doubleA();
doubleA("ab", 2);
nomatch.ts(7,9): error TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type 'boolean' is not assignable to parameter of type 'number'.
nomatch.ts(8,1): error TS2554: Expected 1 arguments, but got 0.
nomatch.ts(9,15): error TS2554: Expected 1 arguments, but got 2.
読み方は次のとおりです。
| 行 | 意味 |
|---|---|
| No overload matches this call. | 並べたシグネチャのどれにも合わなかった |
| The last overload gave the following error. | 最後に試したシグネチャ(ここでは number 版)で出たエラーを代表として表示している |
| Argument of type 'boolean' is not assignable to parameter of type 'number'. | 渡した引数の型と、そのシグネチャが求める型 |
TypeScript 7.0.2 の表示は「最後のシグネチャで出たエラー」だけなので、「number に合わない」と書かれていても、string を渡せば通る場合があります。エラーを読むときは、関数の宣言を開いてシグネチャの一覧を見るのが近道です。引数の個数が合わないときは TS2769 ではなく TS2554 になりました。
同じ nomatch.ts を TypeScript 5.9.3 で型チェックすると、TS2769 はシグネチャごとの内訳つきで表示されました。
nomatch.ts(7,9): error TS2769: No overload matches this call.
Overload 1 of 2, '(value: string): string', gave the following error.
Argument of type 'boolean' is not assignable to parameter of type 'string'.
Overload 2 of 2, '(value: number): number', gave the following error.
Argument of type 'boolean' is not assignable to parameter of type 'number'.
nomatch.ts(8,1): error TS2554: Expected 1 arguments, but got 0.
nomatch.ts(9,15): error TS2554: Expected 1 arguments, but got 2.
npx tsc -p . --locale ja で出した TypeScript 7.0.2 の日本語の文は次のとおりです。
nomatch.ts(7,9): error TS2769: この呼び出しに一致するオーバーロードはありません。
前回のオーバーロードにより、次のエラーが発生しました。
型 'boolean' の引数を型 'number' のパラメーターに割り当てることはできません。
TS2769 が出たときの直し方
| 状況 | 直し方 |
|---|---|
| ユニオン型の値を渡している | 渡す前に typeof などで絞り込む。または、ユニオン型を受けるシグネチャを最後に足す |
| 対応していない型を渡している | 渡す値を変換する。その型を受けたいならシグネチャを足す |
| ライブラリの関数で出た | 宣言を開いてシグネチャの一覧を確かめ、近いものに引数を合わせる |
実装シグネチャは外から呼べない
オーバーロードでは、本体つきの実装のシグネチャは呼び出し側から見えません。 呼び出せるのは、上に並べた宣言だけです。
// impl-hidden.ts
function createId(prefix: string): string;
function createId(prefix: string, seq: number): string;
function createId(prefix: string, seq?: number, width?: number): string {
return seq === undefined ? prefix : `${prefix}-${String(seq).padStart(width ?? 3, "0")}`;
}
createId("T");
createId("T", 7);
createId("T", 7, 5);
impl-hidden.ts(9,18): error TS2554: Expected 1-2 arguments, but got 3.
実装は3つめの引数 width を受け取れますが、宣言には引数1個と2個の形しかないため、3個の呼び出しはエラーになります。前の節で string | number の値が通らなかったのも同じ理由です。実装の形でも呼びたいときは、その形を宣言として足します。
シグネチャの順序で戻り値の型が変わる
シグネチャは上から順に試され、最初に合ったものが使われます。
// order.ts
// 具体的なシグネチャを先に書く
function wrapA(value: string): string[];
function wrapA(value: unknown): unknown[];
function wrapA(value: unknown): unknown[] {
return [value];
}
// 広いシグネチャを先に書く
function wrapB(value: unknown): unknown[];
function wrapB(value: string): string[];
function wrapB(value: unknown): unknown[] {
return [value];
}
const a = wrapA("pen");
const b = wrapB("pen");
console.log(a[0]?.toUpperCase());
console.log(b[0]?.toUpperCase());
order.ts(19,19): error TS2339: Property 'toUpperCase' does not exist on type '{}'.
型定義ファイルで確かめると、a は string[]、b は unknown[] でした。wrapB は先頭の unknown 版に string も合ってしまうため、2つめの string 版まで進みません。具体的な型のシグネチャを上に、広い型のシグネチャを下に書きます。
なお、引数が文字列リテラル型のシグネチャは、順序を逆にしても結果が変わりませんでした。
// order-literal.ts
function findA(key: "id"): number;
function findA(key: string): string;
function findA(key: string): number | string {
return key === "id" ? 1 : "sato";
}
function findB(key: string): string;
function findB(key: "id"): number;
function findB(key: string): number | string {
return key === "id" ? 1 : "sato";
}
const a = findA("id"); // number
const b = findB("id"); // number
const c = findB("name"); // string
コメントの型は TypeScript 7.0.2 の型定義ファイルの出力で、5.9.3 でも同じでした。リテラル型の引数は順序が逆でも先に試されましたが、読む人のためにも、具体的なものを上に書く並べ方でそろえておくと迷いません。
コールバックに渡すと最後のシグネチャが使われる
オーバーロードした関数をそのまま map などに渡すと、最後のシグネチャで型が決まりました。
// callback.ts
function doubleA(value: string): string;
function doubleA(value: number): number;
function doubleA(value: string | number): string | number {
return typeof value === "string" ? value + value : value * 2;
}
const nums = [1, 2, 3].map(doubleA);
const strs = ["a", "b"].map(doubleA);
callback.ts(7,29): error TS2345: Argument of type '{ (value: string): string; (value: number): number; }' is not assignable to parameter of type '(value: string, index: number, array: string[]) => number'.
Type 'string' is not assignable to type 'number'.
number の配列は通り、string の配列でエラーになります。["a", "b"].map((s) => doubleA(s)) とアロー関数で包むと、呼び出しごとにシグネチャが選ばれ、結果は string[] になりました。ReturnType<typeof doubleA> も同じく最後のシグネチャの number になります。
宣言と実装が合わないときのエラー(TS2394・TS2391)
宣言したシグネチャは、実装のシグネチャに収まっている必要があります。
// incompatible.ts
function toLabel(value: string): string;
function toLabel(value: boolean): string;
function toLabel(value: string | number): string {
return String(value);
}
function size(value: string): number;
function size(value: unknown[]): string;
function size(value: string | unknown[]): number {
return value.length;
}
incompatible.ts(2,10): error TS2394: This overload signature is not compatible with its implementation signature.
incompatible.ts(8,10): error TS2394: This overload signature is not compatible with its implementation signature.
1つめは、宣言の引数 boolean を実装の string | number が受け取れないため。2つめは、宣言の戻り値 string が実装の戻り値 number に合わないためです。実装の引数は全シグネチャの引数をまとめたユニオン型に、戻り値も全シグネチャの戻り値をまとめたユニオン型にすると解消します。
宣言だけを書いて実装がない、または宣言と実装の間に別の文が入っていると TS2391 になります。
// missing.ts
function parse(value: string): number;
function parse(value: number): number;
function label(value: string): string;
const x = 1;
function label(value: string | number): string {
return String(value);
}
missing.ts(2,10): error TS2391: Function implementation is missing or not immediately following the declaration.
missing.ts(4,10): error TS2391: Function implementation is missing or not immediately following the declaration.
| エラー | 意味 | 直し方 |
|---|---|---|
| TS2769 | 呼び出しがどのシグネチャにも合わない | 引数を合わせる、絞り込む、シグネチャを足す |
| TS2554 | 引数の個数がどのシグネチャとも違う | 個数を合わせる。実装だけにある引数は宣言に足す |
| TS2394 | 宣言が実装のシグネチャに収まっていない | 実装の引数・戻り値をユニオン型で広げる |
| TS2391 | 実装がない、または宣言の直後にない | 宣言のすぐ下に本体つきの関数を書く |
| TS2322(アロー関数) | アロー関数の戻り値がシグネチャごとの戻り値に合わない | 関数宣言で書く。またはアロー関数の戻り値を any にする |
どの方法を選ぶか
| やりたいこと | 選ぶ方法 | 理由 |
|---|---|---|
| 引数は複数の型を受けるが、戻り値の型は1つ | ユニオン型 | 宣言が1つで済み、ユニオン型の値もそのまま渡せる |
| 引数の型ごとに戻り値の型が決まる(2〜4通り) | オーバーロード | 呼び出し側の型が正確で、実装に as がいらない |
| 引数の個数によって意味が変わる | オーバーロード | 「2個のときは必ず両方必要」のような組み合わせを表せる |
| 渡した型をそのまま戻り値に使う | ジェネリクス | 型の種類が増えてもシグネチャを足さずに済む |
| 型ごとの対応が多く、ユニオン型の値も渡したい | ジェネリクス+条件型 | 呼び出し側は柔軟。実装側は as で型を伝える |
オーバーロードを選んだ場合のチェックリストです。
- 具体的な型のシグネチャを上に、広い型を下に並べている
- 実装の引数と戻り値は、全シグネチャをまとめたユニオン型になっている
- ユニオン型の値を渡す呼び出しがあるなら、ユニオン型を受けるシグネチャを最後に足している
- 実装だけが持つ引数を、呼び出し側が使おうとしていない
よくある質問
Q. オーバーロードは実行時に関数が複数作られますか? A. 作られません。宣言は型チェックのためだけにあり、JavaScript として残るのは実装の1つだけです。引数の型による処理の分岐は、実装の中で typeof や instanceof を使って自分で書きます。
Q. シグネチャはいくつまで並べてよいですか? A. 個数の決まりはありませんが、並べるほど読む側の負担が増えます。4つを超えそうなら、引数をオブジェクト1つにまとめる、ジェネリクスにする、関数を分ける、といった形も検討すると見通しがよくなります。
Q. 省略可能な引数とオーバーロードはどう使い分けますか? A. 「あってもなくてもよい」だけなら省略可能な引数で十分です。「数値を渡すときは桁数も必ず渡す」のように、引数どうしに組み合わせの決まりがあるときにオーバーロードが役立ちます。
Q. TS2769 のエラー文に、試したシグネチャが1つしか出ません。 A. TypeScript 7.0.2 では最後のシグネチャで出たエラーが表示されました。ほかのシグネチャは関数の宣言を開いて確かめます。TypeScript 5.9.3 ではシグネチャごとの内訳が表示されました。
まとめ
- オーバーロードは、宣言を複数並べて最後に実装を1つ書く。呼び出し側は、渡した引数に合うシグネチャの戻り値の型を受け取れる
- 戻り値の型が引数で変わらないならユニオン型、変わるならオーバーロードかジェネリクス+条件型を選ぶ
- ユニオン型の値を渡すとオーバーロードは TS2769 になる。ユニオン型を受けるシグネチャを最後に足すと通る
- 実装シグネチャは外から呼べない。呼べるのは上に並べた宣言だけ
- シグネチャは上から順に試されるので、具体的な型を上、広い型を下に書く
- 宣言が実装に収まっていないと TS2394、実装が直後にないと TS2391 になる
- アロー関数は呼び出しシグネチャを並べた型を付けて書く。戻り値の型が分かれるなら関数宣言が書きやすい
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript オーバーロードの書き方|引数の型で戻り値を変える3つの方法」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。