はじめに
昨日、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");