オーバーロードとは、関数に対して複数の型を定義すること。
複数の型を持つ関数を「オーバーロードされた関数」と呼んだりする。
オーバーロードによって、「渡された引数の数によって各パラメータの型が変わる関数」や「パラメータの型によって返り値の型が変わる関数」を表現できる。
以下の内容は、TypeScript のv3.9.2
で動作確認している。
渡された引数の数によって各パラメータの型が変わる関数
まず、「渡された引数の数によって各パラメータの型が変わる関数」について。
具体的な型を例示すると分かりやすいので、早速、定義してみる。
type HasFoo = { foo: number; }; type HasBar = { bar: string; }; type ReturnFooAndBar = { (arg1: number): HasFoo & HasBar; (arg1: string, arg2: number): HasFoo & HasBar; };
ReturnFooAndBar
によって表現される関数は、引数をいくつ渡すかによって、arg1
の型が変わる。
ひとつの場合はnumber
、ふたつの場合はstring
になる。
返り値の型は固定で、どちらの場合もHasFoo & HasBar
を返す。
このようにtype 型の名前 = {関数の型1; 関数の型2; ...}
と書くことで、ひとつの関数に対して複数の型を定義できる。
次に、オーバーロードされた関数を実装する方法だが、関数宣言と関数式で異なる。
関数宣言
関数宣言の場合、以下のように同じ名前の関数を複数宣言する。
function 関数名 関数の型1 function 関数名 関数の型2 function 関数名 両方の型を満たす実装
ReturnFooAndBar
の場合は以下のようになる。
function returnFooAndBar(arg1: number): HasFoo & HasBar; function returnFooAndBar(arg1: string, arg2: number): HasFoo & HasBar; function returnFooAndBar(arg1: number | string, arg2?: number) { if (typeof arg1 === "number") { return { foo: arg1, bar: "a", }; } return { foo: arg2, bar: arg1, }; } const x: ReturnFooAndBar = returnFooAndBar; // ok returnFooAndBar(9); //ok returnFooAndBar("z", 9); // ok returnFooAndBar(9, 8); // Argument of type '9' is not assignable to parameter of type 'string'.ts(2345) returnFooAndBar("z"); // Argument of type '"z"' is not assignable to parameter of type 'number'.ts(2345)
3 番目のreturnFooAndBar
の宣言で具体的な実装を行っているが、これは、2 つの型の両方を満たすものになっていなければならない。
例えばパラメータは(arg1: number | string, arg2?: number)
になっている。
これは、ひとつめのパラメータはnumber
とstring
両方の可能性があり、ふたつめのパラメータは渡されない可能性があるため、このような記述になっている。
最後に実際にreturnFooAndBar
を使っているが、returnFooAndBar(number)
とreturnFooAndBar(string, number)
以外はエラーになっていることを確認できる。
ただ、この記法だと、TypeScript による型チェックが上手く機能しなかった。
例えばreturnFooAndBar
が空のオブジェクトを返しても、エラーにならない。
その結果、空のオブジェクトであるy
がfoo
やbar
というプロパティを持っていることになってしまっている。
function returnFooAndBar(arg1: number): HasFoo & HasBar; function returnFooAndBar(arg1: string, arg2: number): HasFoo & HasBar; function returnFooAndBar(arg1: number | string, arg2?: number) { return {}; } const x: ReturnFooAndBar = returnFooAndBar; // ok const y = returnFooAndBar(1); // const y: HasFoo & HasBar y.foo; // (property) foo: number y.bar; // (property) bar: string // 実際には foo も bar も undefined console.log(y.foo, y.bar); // undefined undefined
関数式
関数式の場合、関数宣言とはまた違った点に注意しないといけない。
以下のように、絶対に実行されないコードを書く必要があった。
arg1
かarg2
のどちらかはnumber
なので最後のreturn
までコードが到達することはないのだが、これを書かないと「この関数はundefined
を返す可能性がある」と見做され、ReturnFooAndBar
を割り当てることができない。
const returnFooAndBar: ReturnFooAndBar = ( arg1: number | string, arg2?: number, ) => { if (typeof arg1 === "number") { return { foo: arg1, bar: "a", }; } else if (typeof arg2 === "number") { return { foo: arg2, bar: arg1, }; } // 以下のコードはどのようなケースでも実行されないが、これがないと TypeScript がエラーを出す return { foo: 1, bar: arg1, }; }; returnFooAndBar(9); //ok returnFooAndBar("z", 9); // ok returnFooAndBar(9, 8); // Argument of type '9' is not assignable to parameter of type 'string'.ts(2345) returnFooAndBar("z"); // Argument of type '"z"' is not assignable to parameter of type 'number'.ts(2345)
関数宣言とは異なり、ReturnFooAndBar
に一致しない実装をすると、エラーを出してくれる。
const returnFooAndBar: ReturnFooAndBar = ( arg1: number | string, arg2?: number, ) => { if (typeof arg1 === "number") { return { foo: arg1, bar: 1, // string であるべきなのに number になっているので、エラーになる }; } else if (typeof arg2 === "number") { return { foo: arg2, bar: arg1, }; } // 以下のコードはどのようなケースでも実行されないが、これがないと TypeScript がエラーを出す return { foo: 1, bar: arg1, }; };
パラメータの型によって返り値の型が変わる関数
次は、パラメータの型によって返り値の型が変わる関数を、オーバーロードで表現する。
型エイリアスで表現する方法は、先程と変わらない。
type HasFoo = { foo: number; }; type HasBar = { bar: string; }; type ReturnFooOrBar = { (arg: number): HasFoo; (arg: string): HasBar; };
ReturnFooOrBar
は、number
を渡されたらHasFoo
を、string
を渡されたらHasBar
を返す。
関数宣言
関数宣言で実装する方法も、先程と変わらない。まずは型を宣言し、最後に両方の型を満たす実装を書く。
function returnFooOrBar(arg: number): HasFoo; function returnFooOrBar(arg: string): HasBar; function returnFooOrBar(arg: string | number) { if (typeof arg === "number") { return { foo: arg, }; } return { bar: arg, }; } const x: ReturnFooOrBar = returnFooOrBar; // ok const y = returnFooOrBar(3); // const y: HasFoo const z = returnFooOrBar("e"); // const z: HasBar console.log(y); // { foo: 3 } console.log(z); // { bar: "e" }
空のオブジェクトを返してもエラーが出ないのも、先程と同じ。
function returnFooOrBar(arg: number): HasFoo; function returnFooOrBar(arg: string): HasBar; function returnFooOrBar(arg: string | number) { return {}; } const x: ReturnFooOrBar = returnFooOrBar; // ok const y = returnFooOrBar(3); // const y: HasFoo const z = returnFooOrBar("e"); // const z: HasBar console.log(y); // {} console.log(z); // {}
関数式
関数式ではそもそも、returnFooOrBar
を定義できなかった。
例えば以下のreturnFooOrBar
の実装は、先程の関数宣言のものと同じなのだが、ReturnFooOrBar
にはならない。
返り値の型が{foo: number;} | {bar: string;}
になってしまっており、パラメータの型によって返り値の型を特定する、ということが出来ていない。
const returnFooOrBar = (arg: number | string) => { if (typeof arg === "number") { const result = { foo: arg, }; return result; } const result = { bar: arg, }; return result; }; const x: ReturnFooOrBar = returnFooOrBar; // error const y = returnFooOrBar(3); // const y: {foo: number;} | {bar: string;} const z = returnFooOrBar("e"); // const z: {foo: number;} | {bar: string;} console.log(y); // { foo: 3 } console.log(z); // { bar: "e" }