TypeScriptの関数オーバーロードとは、1つの関数に呼び出しシグネチャを複数書き、そのあとに実装を1つだけ書くことです。シグネチャごとに引数の型と戻り値の型の組み合わせを変えられ、呼び出し側は正確な型を受け取れます。
オーバーロードがなければ、parse はどの呼び出しでも number | number[] を返し、自分で結果を絞り込むまで one + 1 はエラーになります。
オーバーロードシグネチャと実装
オーバーロードされた関数は2つの部分からできています:
- オーバーロードシグネチャ: 本体のない宣言で、対応する呼び出しの形ごとに1つ書きます。呼び出し側が使えるのはこのシグネチャだけです。
- 実装シグネチャ: 本体を持つ最後の宣言です。引数はオーバーロードが受け付けるものをすべて受け付け、戻り値の型はすべてのオーバーロードの戻り値の型を含まなければなりません。外からは見えません。
型はコンパイル時にしか存在しないので、実行時のJavaScriptの関数は1つだけです。実装は引数を調べて(typeof、Array.isArray、arguments.length など)何をするかを決めます。オーバーロードと実装が一致しているかはコンパイラーがチェックします:
function format(value: string): string;
function format(value: number): number {
return value;
}
// error TS2394: This overload signature is not compatible with its implementation signature.
実装の型を広げれば解決します: function format(value: string | number): string | number。
実装シグネチャは呼び出せない
いちばん意外に感じられるルールです。呼び出しは、オーバーロードシグネチャのどれか1つにそれだけで一致しなければなりません。TypeScriptはシグネチャを組み合わせてはくれません。
コンパイラーの出力は次のとおりです:
index.ts(12,19): error TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type 'string | string[]' is not assignable to parameter of type 'string[]'.
Type 'string' is not assignable to type 'string[]'.
実装は string | string[] を受け付けますが、呼び出し側からは見えません。ユニオン型を受け取ってユニオン型を返す3つ目のオーバーロードを追加すると、呼び出しはコンパイルでき、[ 1, 2 ] が出力されます:
function parse(input: string): number;
function parse(input: string[]): number[];
function parse(input: string | string[]): number | number[];
function parse(input: string | string[]): number | number[] {
return Array.isArray(input) ? input.map(Number) : Number(input);
}
引数の数が違う場合
オーバーロードは引数の数が違う呼び出しも表せます。次の例では、日付をタイムスタンプからも、年・月・日からも作れますが、2つの数値からは作れません:
省略可能な引数を2つ持つ1つのシグネチャでは makeDate(2024, 3) が通ってしまい、黙って間違った日付が作られます。オーバーロードならこれがコンパイルエラー(TS2575)になります。
順序が重要
TypeScriptはオーバーロードを上から順に試し、最初に一致したものを選びます。具体的なシグネチャを先に書きましょう。広いオーバーロードを先に書くと、後ろのシグネチャ向けの呼び出しまで吸い込んでしまいます:
function describe(value: unknown): string; // matches everything
function describe(value: string): "text"; // never chosen
function describe(value: unknown): string {
return typeof value === "string" ? "text" : "other";
}
const d = describe("hi"); // d: string, not "text"
最初の2つのシグネチャを入れ替えると、describe("hi") の型は "text" になります。
オーバーロードかユニオン型の引数か
オーバーロードが行数に見合うのは、引数の型によって戻り値の型が変わるときです。そうでなければ、ユニオン型の引数を持つ1つのシグネチャのほうが短く読みやすく、オーバーロードでは拒否されるユニオン型の引数も受け付けます。
| 使うもの | 使う場面 |
|---|---|
| ユニオン型の引数 | どの入力でも戻り値の型が同じ |
| 省略可能な引数 | 呼び出しの形の違いが、自由に省略できる後ろの引数だけ |
| オーバーロード | 引数によって戻り値の型が変わる、または一部の引数の組み合わせを拒否したい |
| ジェネリクス | 戻り値の型が引数の型から作られる(identity<T>(x: T): T など) |
ジェネリクスと 条件型 を組み合わせると、一部のオーバーロードは1つのシグネチャで表せますが、ケースが2つか3つならオーバーロードのほうがたいてい読みやすくなります。
メソッドとコンストラクターのオーバーロード
クラスの中のメソッドも同じパターンで書きます。オーバーロードシグネチャのあとに本体を持つメソッドを書きます。コンストラクターも同じようにオーバーロードできます。
インターフェースやオブジェクト型でも、複数の呼び出しシグネチャや同じ名前の複数のメソッドシグネチャとしてオーバーロードを宣言できます。組み込み関数の多くはこの方法で宣言されています。エディターで配列の reduce にカーソルを合わせると「+2 overloads」と表示されます。
よくある質問
TypeScriptは関数のオーバーロードに対応していますか?
はい、型のレベルで対応しています。オーバーロードシグネチャ(本体のない宣言)を複数書き、そのあとに実装を1つ書きます。呼び出し側に見えるのはオーバーロードシグネチャだけです。実行時のJavaScriptの関数は1つだけなので、実装が自分で引数を調べてすべての場合を処理します。
「No overload matches this call」とはどういう意味ですか?
エラー TS2769 で、引数がどのオーバーロードシグネチャにも当てはまらないという意味です。実装シグネチャは数に入らないので、string | string[] のようなユニオン型の引数を渡すと、実装が受け付けるとしても失敗します。ユニオン型を受け取るオーバーロードを追加するか、オーバーロードをやめて1つのシグネチャにします。
ユニオン型ではなくオーバーロードを使うべきなのはどんなときですか?
渡す引数の型によって戻り値の型が変わるときです。たとえば string を渡せば number が返り、string[] を渡せば number[] が返る場合です。どの入力でも戻り値の型が同じなら、ユニオン型の引数を持つ1つのシグネチャのほうがシンプルで、ユニオン型の引数も受け付けられます。
TypeScriptでアロー関数をオーバーロードできますか?
オーバーロード宣言の構文ではできません。この構文は function 宣言とメソッドでしか使えません。変数に複数の呼び出しシグネチャを持つ型(type Parse = { (s: string): number; (s: string[]): number[] })を付けることはできますが、アロー関数を代入するにはたいてい型アサーションが必要になるので、function 宣言のほうがすっきりします。
オーバーロードシグネチャが実装シグネチャと互換でないと言われるのはなぜですか?
エラー TS2394 は、あるオーバーロードが受け取る値や返す値を実装が扱えないという意味です。実装の引数はすべてのオーバーロードの引数を受け付け、実装の戻り値の型はすべてのオーバーロードの戻り値の型と互換でなければなりません。実装の型を広げれば(多くはユニオン型に)解決します。