オーバーロード関数 (overload function)
オーバーロード関数(overload function)は、TypeScriptの機能で、ひとつの関数に異なる関数シグネチャを複数もつ関数です。関数シグネチャとは、どのような引数を取るか、どのような戻り値を返すかといった関数の型のことです。要するに、異なる引数や戻り値のパターンがいくつかある関数をオーバーロード関数と言います。
オーバーロード関数の文法
TypeScriptでは、オーバーロード関数は、関数シグネチャと実装の2つの部分に分けて書きます。
ts
// 関数シグネチャ部分functionhello (person : string): void; // シグネチャ1functionhello (persons : string[]): void; // シグネチャ2// 関数の実装部分functionhello (person : string | string[]): void {if (typeofperson === "string") {console .log (`Hello ${person }`);} else {console .log (`Hello ${person .join (",")}`);}}
ts
// 関数シグネチャ部分functionhello (person : string): void; // シグネチャ1functionhello (persons : string[]): void; // シグネチャ2// 関数の実装部分functionhello (person : string | string[]): void {if (typeofperson === "string") {console .log (`Hello ${person }`);} else {console .log (`Hello ${person .join (",")}`);}}
関数シグネチャの部分は、オーバーロードのパターン数だけ複数書きます。この部分はインターフェースを定義するところなので、関数のボディは書けません。
関数の実装部分は、オーバーロードの全パターンを網羅する関数を書きます。ありうる引数の数や型のパターンを網羅したものになります。ロジックも分岐などを用いて、パターンごとの処理を書く必要があります。
関数シグネチャと実装部分の関数名は同じにする必要があります。
オーバーロード関数の文法を一般化すると次のようになります。
js
function 関数名 関数シグネチャ1function 関数名 関数シグネチャ2function 関数名 すべてのシグネチャを網羅する実装
js
function 関数名 関数シグネチャ1function 関数名 関数シグネチャ2function 関数名 すべてのシグネチャを網羅する実装
オーバーロード関数のコンパイル結果
オーバーロード関数はTypeScriptからJavaScriptにコンパイルすると、関数シグネチャ部分と型注釈が消され、次のようなコードになります。
コンパイル後のJavaScriptts
function hello(person) {if (typeof person === "string") {console.log(`Hello ${person}`);}else {console.log(`Hello ${person.join(",")}`);}}
コンパイル後のJavaScriptts
function hello(person) {if (typeof person === "string") {console.log(`Hello ${person}`);}else {console.log(`Hello ${person.join(",")}`);}}
なぜJavaのようなオーバーロードではないのか?
オーバーロードは他の言語にもあります。たとえば、Javaを始めとするJVM言語、C#、Swiftなどです。これらの言語のオーバーロードを知っていると、TypeScriptのオーバーロードは独特に思えるかもしれません。
他の言語のオーバーロードの書き方は、シグネチャごとに実装が書けます。たとえば、JVM言語のひとつのKotlinでは、次のように書けます。TypeScriptと比べると、シグネチャごとに実装が別れていて、if分岐もなく、読みやすいのではないでしょうか。
Kotlinのオーバーロード関数kotlin
fun hello(person: String) {println("Hello $person")}fun hello(persons: Array<String>) {println("Hello ${persons.joinToString(",")}")}
Kotlinのオーバーロード関数kotlin
fun hello(person: String) {println("Hello $person")}fun hello(persons: Array<String>) {println("Hello ${persons.joinToString(",")}")}
では、なぜTypeScriptは、このような書き方を採用しなかったのでしょうか。理由として、JavaScriptにオーバーロードがない点が挙げられます。
TypeScriptで書いたコードは「型に関する部分を消したらJavaScriptになる」というのがTypeScriptの基本方針です。このおかげで、開発者はTypeScriptコードが意図したJavaScriptにコンパイルされたかを確認する必要がなく、TypeScriptコードを見るだけでJavaScriptコードが予測できるという利点があります。
もしも、Javaのようなオーバーロードを採用すると、JavaScriptにオーバーロードがない以上、どの関数を呼ぶかといった解決ロジックをTypeScriptがコンパイル時に生成しなければなりません。そうなると、TypeScriptの基本方針から大きく外れてしまいます。ソースコードからの予測可能性も下がります。こうしたことから、TypeScriptのオーバーロードは関数シグネチャ定義にとどめていると見られます。
アロー関数とオーバーロード
オーバーロード関数の構文が用意されているのは関数宣言だけです。アロー関数にはオーバーロードの構文がありません。アロー関数でオーバーロード関数を作るには、関数呼び出しシグネチャで型注釈する必要があります。
ts
// 関数呼び出しシグネチャでHello型を定義typeHello = {(person : string): void;(persons : string[]): void;};// Hello型で型注釈consthello :Hello = (person : string | string[]): void => {if (typeofperson === "string") {console .log (`Hello ${person }`);} else {console .log (`Hello ${person .join (",")}`);}};
ts
// 関数呼び出しシグネチャでHello型を定義typeHello = {(person : string): void;(persons : string[]): void;};// Hello型で型注釈consthello :Hello = (person : string | string[]): void => {if (typeofperson === "string") {console .log (`Hello ${person }`);} else {console .log (`Hello ${person .join (",")}`);}};
関数呼び出しシグネチャ以外に、関数型(function type)とインターセクション型を用いる方法もあります。
ts
// 関数型とインターセクション型を用いてHello型を定義typeHello = ((person : string) => void) & ((persons : string[]) => void);consthello :Hello = (person : string | string[]): void => {if (typeofperson === "string") {console .log (`Hello ${person }`);} else {console .log (`Hello ${person .join (",")}`);}};
ts
// 関数型とインターセクション型を用いてHello型を定義typeHello = ((person : string) => void) & ((persons : string[]) => void);consthello :Hello = (person : string | string[]): void => {if (typeofperson === "string") {console .log (`Hello ${person }`);} else {console .log (`Hello ${person .join (",")}`);}};
関数シグネチャは詳しい順に書く
オーバーロードの関数シグネチャは順番が重要になります。TypeScriptは関数シグネチャを上から順に試していき、最初にマッチしたシグネチャを採用します。そのため、より詳しい関数シグネチャが上に、詳しくないものが下に来るように書き並べなければなりません。詳しいとは、引数の型の範囲が狭いという意味です。たとえば、number
より1 | 2
のほうが狭い型です。any
はnumber
より広い型です。
ts
functionfunc (param : 1 | 2): 1 | 2; // 詳しい関数functionfunc (param : number): number; // そこそこ詳しい関数functionfunc (param : any): any; // 詳しくない関数functionfunc (param : any): any {// ...}constresult1 =func (1);constresult2 =func (100);constresult3 =func ("others");
ts
functionfunc (param : 1 | 2): 1 | 2; // 詳しい関数functionfunc (param : number): number; // そこそこ詳しい関数functionfunc (param : any): any; // 詳しくない関数functionfunc (param : any): any {// ...}constresult1 =func (1);constresult2 =func (100);constresult3 =func ("others");
次の誤りのように、詳しい関数を下のほうに書いてしまうと、詳しい関数がまったく採用されなくなります。
誤り:シグネチャの順番が間違っているts
functionfunc (param : any): any; // 詳しくない関数。採用されるfunctionfunc (param : 1 | 2): 1 | 2; // 詳しい関数。採用されないfunctionfunc (param : any): any {// ...}constresult =func (1);
誤り:シグネチャの順番が間違っているts
functionfunc (param : any): any; // 詳しくない関数。採用されるfunctionfunc (param : 1 | 2): 1 | 2; // 詳しい関数。採用されないfunctionfunc (param : any): any {// ...}constresult =func (1);
オーバーロード以外も検討しよう
オーバーロード関数以外の方法を使ったほうがいい場合もあります。
代わりにオプション引数を使う
引数の数が違うだけの場合、オーバーロードよりオプション引数を使ったほうがよいです。たとえば、次のようなオーバーロード関数は、strictNullChecksが有効な場合、第2引数にundefined
が渡せません。
オーバーロードを使う例ts
functionfunc (one : number): void;functionfunc (one : number,two : number): void;functionfunc (one : number,two ?: number): void {}Argument of type 'undefined' is not assignable to parameter of type 'number'.2345Argument of type 'undefined' is not assignable to parameter of type 'number'.func (1,); undefined
オーバーロードを使う例ts
functionfunc (one : number): void;functionfunc (one : number,two : number): void;functionfunc (one : number,two ?: number): void {}Argument of type 'undefined' is not assignable to parameter of type 'number'.2345Argument of type 'undefined' is not assignable to parameter of type 'number'.func (1,); undefined
これは次のようにオプション引数を使うだけにとどめたほうがよいです。
オプション引数を使う例ts
functionfunc (one : number,two ?: number): void {}func (1,undefined );
オプション引数を使う例ts
functionfunc (one : number,two ?: number): void {}func (1,undefined );
代わりにユニオン型を使う
引数の型だけが異なる場合は、ユニオン型を使ったほうがシンプルです。
オーバーロードを使う例ts
functionfunc (x : string): void;functionfunc (x : number): void;functionfunc (x : string | number) {}
オーバーロードを使う例ts
functionfunc (x : string): void;functionfunc (x : number): void;functionfunc (x : string | number) {}
ユニオン型を使う例ts
functionfunc (x : string | number) {}
ユニオン型を使う例ts
functionfunc (x : string | number) {}
代わりにジェネリクスを使う
引数の型と戻り値の型に一定の対応関係がある場合は、ジェネリクスを使ったほうがシンプルになる場合があります。
オーバーロードを使う例ts
functionfunc (x : boolean): boolean;functionfunc (x : number): number;functionfunc (x : string): string;functionfunc (x : boolean | string | number): boolean | string | number {returnx ;}
オーバーロードを使う例ts
functionfunc (x : boolean): boolean;functionfunc (x : number): number;functionfunc (x : string): string;functionfunc (x : boolean | string | number): boolean | string | number {returnx ;}
ジェネリクスを使う例ts
functionfunc <T extends boolean | number | string>(x :T ):T {returnx ;}
ジェネリクスを使う例ts
functionfunc <T extends boolean | number | string>(x :T ):T {returnx ;}