TypeScriptのoverloadでシグネチャの場合分けを実現する方法

フロントエンド

はじめに

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の濫用に注意

ただし濫用には注意が必要です。
以下のパターンは他の解決策を考えた方が良いかもしれません。

  1. 引数と戻り値に単純な相関がある場合はジェネリクスを使う。
    特に引数と戻り値の型が全く同じになる場合はジェネリクスの方が良い。
  2. シグネチャのパターンが少なくoverloadで絞る必要がない場合はユニオン型で十分
  3. 引数の方が違うだけならオプション引数でOK
  4. 引数による動作のパターン分けが多くなってきた場合は実装を分ける

具体例を示しておきます。

引数と戻り値に単純な相関がある場合はジェネリクスを使う

// 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を使ってる人は少ない気がするので、適切に設定してチームをハッピーにしましょう!

タイトルとURLをコピーしました