はじめに
TypeScriptのoverload
関数が結構便利だったので、ログ残しときます。
jsではなくあくまでTypeScriptの機能なので、他の言語とoverload
の振る舞いが若干違うので注意が必要です。
適切にoverloadを設定できると可読性も上がるしエディタの補完的にも嬉しいので是非身につけたいです。
overloadについて
どんな時に使えるか
typescriptで関数を実装する際に、引数・戻り値に型をつけると思います。
その際に、その組み合わせの引数で関数呼び出しはできるけど、想定してない呼び出し方だよみたいなことあるかと思います。
その時にコメントアウトで注意書きではなくoverload
を使いましょう
具体的に例えば以下のような関数を作るとします。
function callApi(method: "get" | "post", url: string, body?: Object): any {
// do something ...
}
APIを叩く関数で、method
にgetもしくはpostを指定することができます。
ただし、getを指定した場合は、body
は必要ないのでundefinedを許容するように設定しています。
この時、引数がとりうるパターンは以下の通りです。
method | url | body | |
---|---|---|---|
“get” | string | Object | 必要ない |
“get” | string | undefined | 必要 |
“post” | string | Object | 必要 |
“post” | string | undefined | 必要ない ( 指定しないなら明示的に空オブジェクトを渡してほしい ) |
上のテーブルで「必要ない」となっているパターンでは呼び出してほしくない時にoverload
が活躍します。
具体的には以下のように書くことで引数のパターンを制限することができます。
// method = "get"のとき body は undefined でしか受け付けない
function callApi(method: "get", url: string, body: undefined): any
// method = "post"のとき body は Object でしか受け付けない
function callApi(method: "post", url: string, body: Object): any
function callApi(method: "get" | "post", url: string, body?: Object): any {
// 実際の処理を実装できるのはここだけ
if (method === "get"){
// call get API ...
} else {
// call post API ...
}
}
このようにすることでシグネチャ(引数・戻り値の型)のパターンを絞ることができます。
注意点は以下です。
- typescriptの
overload
は実装箇所は1箇所のみ overload
はシグネチャを絞ることが目的なので、実装部のシグネチャは緩く、overload
部のシグネチャは厳しくなるようにする- 記述する順番はよりシグネチャが厳しいものを上にする。
( overloadのoverload的なこともできる。たぶん読みにくくなるからオススメしない。 ) - アロー関数は使用できない。function形式で関数を定義する必要がある。
さらにoverloadしてみる
リテラル型などを駆使すればさらにシグネチャを絞ることができます。
function callApi(method: "get", url: `/users`, body: undefined): User[]
function callApi(method: "get", url: `/users/${number}`, body: undefined): User
function callApi(method: "get", url: `/posts`, body: undefined): Post[]
function callApi(method: "get", url: `/posts/${number}`, body: undefined): Post
このように記述することでurl
引数に指定可能な文字列パターン・戻り値の型を限定することができます。getUsers
, getUser
, getPosts
, getPost
など、エンドポイントが違うだけで動作が同じ関数をたくさん作る必要がないです。
overloadの濫用に注意
ただし濫用には注意が必要です。
以下のパターンは他の解決策を考えた方が良いかもしれません。
- 引数と戻り値に単純な相関がある場合はジェネリクスを使う。
特に引数と戻り値の型が全く同じになる場合はジェネリクスの方が良い。 - シグネチャのパターンが少なく
overload
で絞る必要がない場合はユニオン型で十分 - 引数の方が違うだけならオプション引数でOK
- 引数による動作のパターン分けが多くなってきた場合は実装を分ける
具体例を示しておきます。
引数と戻り値に単純な相関がある場合はジェネリクスを使う
// overload
function f(arg: number): number
function f(arg: string): string
function f(arg: number | string): number | string {}
// ジェネリクス
function f(arg: T): T {}
シグネチャのパターンが少なくoverloadで絞る必要がない場合はユニオン型で十分
// overload
function f(x: string): void;
function f(x: number): void;
function f(x: string | number): void {}
// ジェネリクスだけで十分
function f(x: string | number): void {}
引数の方が違うだけならオプション引数でOK
// overload
function f(arg1: number): void;
function f(arg1: number, arg2: number): void;
function f(arg1: number, arg2?: number): void {}
// これもパターンを限定できてる訳ではないので、overloadじゃなくてOK
function f(arg1: number, arg2?: number): void {}
引数による動作のパターン分けが多くなってきた場合は実装を分ける
// overload
function f(method: "put", model: "user", object: User): User
function f(method: "put", model: "tweet", object: Tweet): Tweet
function f(method: "post", model: "user", object: User): User
function f(method: "post", model: "tweet", object: Tweet): Tweet
// バリデーションとか入れたいから引数によって動作を分ける必要がある。
function f(method: "put" | "post", model: "user" | "tweet", object: Object): any {
if (arg === "get") {
if (model === "user"){ /* ... */ }
else { /* ... */ }
} else {
if (model === "user"){ /* ... */ }
else { /* ... */ }
}
}
// 実装部を分ける ( 高階関数とか使っても分かりやすくなるかも )
function sendUser(method: "put" | "post", object: User): User {}
function sendTweet(method: "put" | "post", object: Tweet): Tweet {}
意外と現場でoverload
を使ってる人は少ない気がするので、適切に設定してチームをハッピーにしましょう!