はじめに
昨日、TypeScript のオーバーロードの様々な書き方についてまとめてみました。
本日は、オーバーロードの挙動確認中に遭遇した不思議な仕様について紹介します。
環境
TypeScript: v4.1.3
コード
結論
以下のように返り値の異なるオーバーロードを定義する場合、実装関数の返り値の型が union type (number | string)だと、Type 'string' is not assignable to type 'number'というエラーが表示されます。
type T = {
(val: number): number;
(val: string): string;
};
// Type 'string' is not assignable to type 'number'
const fn: T = (val: number | string) => val;
そのため、実装関数の返り値の型を any にする必要があります。
// No Error
const fn: T = (val: number | string): any => val;
// const num: number
const num = fn(1);
// const str: string
const str = fn("1");
詳細
具体例を見てみましょう。
以下で定義されているTは、引数にstring | numberを受け取り、受け取った引数をそのまま返す関数になります。
当然、返り値の型はstring | numberとなります。
// const fn: (val: number | string) => string | number
const fn = (val: number | string) => val;
// const num: string | number
const num = fn(1);
// const str: string | number
const str = fn("1");
この関数にオーバーロードを使用して、以下のような型を返すようにします。
- 引数の型が
stringであれば、返り値の型はstring - 引数の型が
numberであれば、返り値の型はnumber
そうすると以下のようになります。
しかし、このように定義した場合、以下に記述したようなエラーが表示されます。
type T = {
(val: number): number;
(val: string): string;
};
// Error
// Type '(val: number | string) => string | number' is not assignable to type 'T'.
// Type 'string | number' is not assignable to type 'number'.
// Type 'string' is not assignable to type 'number'.ts(2322)
const fn: T = (val: number | string) => val;
// const num: number
const num = fn(1);
// const str: string
const str = fn("1");
これを解消するには、返り値の型をanyにする必要があります。
type T = {
(val: number): number;
(val: string): string;
};
// No Error
const fn: T = (val: number | string): any => val;
// or const fn: T = (val: number | string) => val as any;
// const num: number
const num = fn(1);
// const str: string
const str = fn("1");
TypeScript のオーバーロードの仕様上、オーバーロードを使用している関数 (この場合はfn)は、それぞれの型を同時に満たす必要があります。
const num: number = fn(1);
const str: string = fn("1");
つまり、引数の型はnumber | string である必要があり、返り値の型はnumber & stringである必要があるようです。
参考: Overloading arrow function return type should allow union type
実際、以下のようにfnを定義すると、エラーが消えます。
const fn: T = (val: number | string): number & string => val as never;
また、この挙動は呼び出し可能オブジェクト(Callable)を用いてオーバーロードを定義した場合のみ発生し、functionを用いてオーバーロードした場合は発生しません。
function fn(val: number): number;
function fn(val: string): string;
function fn(val: number | string) {
return val;
}
// const num: number
const num = fn(1);
// const str: string
const str = fn("1");