TypeScript のみの開発ではオーバーロード(関数の型定義を複数書く)はあまり使わないと思うが、JavaScript のコードに対して d.ts ファイルを書く時にしばしばオーバーロードが使われると思う。
オーバーロードをするときは正しい型定義しないとうまく型が効いてくれなくて困ったりしたことはないだろうか(困った)。
なのでオーバーロード時の型定義をおさらいしたい。
結論
- オーバーロードの型を書くときは厳しい型から緩い型の順に書け(基本)
- すなわち、最後には全てを含む最も緩い型を書け(?)
って公式にも書いてある。
https://www.typescriptlang.org/docs/handbook/functions.html#overloads
理由
TypeScript が関数の型を解釈するときに、当てはまるまで上から順番に見ていくから。
厳しい型が当てはめれるならば、当てはめた方が良いよね。
緩い型から書いてしまうと、本当はもっと厳しい型にできるのに、緩いほうに適用されてしまうことになる。
という基本的なことを前提にして、以下のような型定義があったとしたらどうだろう。
export class Hoge {
constructor(id: string, options: Options)
constructor(options: Options)
この型定義は実は不十分(?)。正しいのはこれ。
export class Hoge {
constructor(id: string, options: Options)
constructor(options: Options)
constructor(arg1: string | Options, arg2?: Options)
最後に 今までの型全てを含む最もゆるい型定義 を書いている。
前者の型定義だと、例えば以下のようなコードで型エラーが起きてしまっていました。
ConstructorParameters でコンストラクターの引数の型を取得できるが、その際、オーバーロードされていると一番下に定義した型が適用されるらしい( 前者だと constructor(options: Options)
)。
後者の型定義だとエラーは出ません。
なので、オーバーロードで型定義するときは最後に今までの型を含む最も緩い型を書くのが正義と言えそうだ。
おしまい。
反論
ここで終わろうとして気づいたが、最後に緩い型を作ってしまうと、例えば以下のようなコードで怒ってくれなくなる。
const hoge = new Hoge('id')
本来はこのidだけを渡す引数は認めたくない (元のjavascript 的にも実行時エラーになる) ときは不便。
こういったケースはたまーにあると思うのだけれど、どうしたものだろうか...。
まあ、元のJavaScriptがひどいシグネチャ設計なのがいけないと言われたらそうだと思う。