TSの関数のオーバーロード
TSで関数のオーバーロードができるのは知っていたしライブラリの実装として見たことはあったが、いい感じに使う場面がイマイチ不明だった。
実務で使う機会があって勉強になったのでまとめておく。
関数のオーバーロードとは
TSではある関数を作成したときに引数の型と返り値の型を定義する。返り値の型は推論に任せる場合もある。
関数のオーバーロードを使うと同じ関数の名前で「引数の型によって返り値の型が変わる関数」だったり、「引数の数によって返り値の型が変わる関数」というのを表現できるようになる。
具体例で見ていく。
以下のtest関数は第2引数がある場合は第2引数を、ない場合は第1引数をそのまま返す関数である。
第1引数、第2引数はそれぞれnumber
とstring
なので返り値の型はユニオン型のnumber | string
になる
しかし実装内容を考慮すると、実際にはこの関数の返り値は第1引数だけを与えた場合はnumber
になるし、第2引数まで与えた場合はstring
になる。
このような実際に使う際の引数の型と返り値の型の組み合わせを柔軟に変えられるのが関数のオーバーロードである。
実際にオーバーロードを使って見た例がこちら。
先程実装した関数の実装の上に、同じ名前で引数の型と返り値の型を定義すればよい。
ちなみにオーバーロードする部分は実装部分の返り値の型がstring | number
のユニオン型なので、このどちらかである必要がありboolean
を返り値にするなどはできない。
逆に言えば、string | number
のユニオン型を返す場合に本当はstring
を返すのにnumber
を返すみたいな嘘をつけてしまうのでここは注意が必要。
(上記の例だと、2行目をfunction test(arg1: number, arg2: string):number
としてもエラーにならず、11行目の型はnumber
になってしまう)
実際のプロダクトで使ってみる
使い方は理解できたので、もう少し実用的な例で考えてみる。
今回はfirebase-admin/firestore
にあるFirebaseの型定義を用いる。
firestoreではQueryDocumentSnapshot
とDocumentSnapshot
という2つの型があり型定義を見ると前者は後者をextendsしている。
違いとしてはこれらのsnapshotから実際のデータを取り出すときにsnap.data()
としてDocumentData
を得るのだが、この返り値がQueryDocumentSnapshot
の場合はundefinedは含まれないがDocumentSnapshot
の場合はundefinedが含まれる。
これを関数のオーバーロードを使って1つにまとめてみる。
QueryDocumentSnapshot
はDocumentSnapshot
をextendsしているものなので、実装部分はDocumentSnapshot
にしておけばオーバーロードでQueryDocumentSnapshot
を書いてもエラーにはならない。
これだけだとあまり嬉しくないのでここに返り値のデータにジェネリクスの型を返すような実装にしてみる。
実装部分の返り値の型をunknown
にして、オーバーロード部分に型パラメータを追加してそれを返り値に設定するようにした。
このように実装しておけば実際にこの関数を使うときに型パラメータを渡してやることで返り値の型がそれに対応するようになる。
厳密には違うのかもしれないが、ここでやったオーバーロードは実質下記のような型アサーションと同じようなことをやっているという認識でいる。
実装部分とインターフェース部分の分離
先程のオーバーロードでまとめた部分で2行目と3行目は同じ型を書いている。
最初2行目は不要な気もしたが実際には必要である。
2行目をコメントアウトして引数にDocumentSnapshot
を与えるとエラーになったことから、3行目はあくまでも実装とその実装内容を満たす型定義とする必要があって、オーバーロードした場合は使う側ではオーバーロードされた型定義に従うという認識でいる。
表現として適切かは分からないが、関数のオーバーロードをした場合、実装部分とインターフェース部分が分かれてオーバーロード部分がインターフェースの定義としての役割を担う的なイメージでいる。
オーバーロードの適用順序
オーバーロードの適用順序は上から順に適用される。
つまり、先程のオーバーロードの1行目と2行目を入れ替えて、QueryDocumentSnapshot
の引数を与えても1行目のオーバーロードが適用されてしまう。
(QueryDocumentSnapshot
はDocumentSnapshot
をextendsしているので1行目に当てはまってしまう)
まとめ
存在は知っていてもそれを実際に有意義に使えるかとなると使えないものも多い。
関数のオーバーロードもその1つだったが、今回でちゃんと使い方と注意すべき点を理解できたと思う。
あまり乱用すべきではないと思うし使い所も多くはないかなと思うが、ライブラリの実装ではよく使われる気がしているので今後見る際に正しく読み取れそう。いい勉強になった。
参考
あわせて読みたい
https://www.typescriptlang.org/docs/handbook/2/functions.html