TSの関数のオーバーロード

2022 5/18

TSで関数のオーバーロードができるのは知っていたしライブラリの実装として見たことはあったが、いい感じに使う場面がイマイチ不明だった。
実務で使う機会があって勉強になったのでまとめておく。

目次

関数のオーバーロードとは

TSではある関数を作成したときに引数の型と返り値の型を定義する。返り値の型は推論に任せる場合もある。

関数のオーバーロードを使うと同じ関数の名前で「引数の型によって返り値の型が変わる関数」だったり、「引数の数によって返り値の型が変わる関数」というのを表現できるようになる。

具体例で見ていく。
以下のtest関数は第2引数がある場合は第2引数を、ない場合は第1引数をそのまま返す関数である。
第1引数、第2引数はそれぞれnumberstringなので返り値の型はユニオン型のnumber | stringになる

function test(arg1: number, arg2?: string) {
  return arg2 || arg1
}

// 返り値の型はstring | number
const result = test(100) 

しかし実装内容を考慮すると、実際にはこの関数の返り値は第1引数だけを与えた場合はnumberになるし、第2引数まで与えた場合はstringになる。

このような実際に使う際の引数の型と返り値の型の組み合わせを柔軟に変えられるのが関数のオーバーロードである。
実際にオーバーロードを使って見た例がこちら。

function test(arg1: number): number
function test(arg1: number, arg2: string): string
function test(arg1: number, arg2?: string): string | number {
  return arg2 || arg1
}

// 返り値の型はnumber
const numberResult = test(100)

// 返り値の型はstring
const stringResult = test(100, "foo")

先程実装した関数の実装の上に、同じ名前で引数の型と返り値の型を定義すればよい。
ちなみにオーバーロードする部分は実装部分の返り値の型がstring | numberのユニオン型なので、このどちらかである必要がありbooleanを返り値にするなどはできない。
逆に言えば、string | numberのユニオン型を返す場合に本当はstringを返すのにnumberを返すみたいな嘘をつけてしまうのでここは注意が必要。
(上記の例だと、2行目をfunction test(arg1: number, arg2: string):number としてもエラーにならず、11行目の型はnumberになってしまう)

実際のプロダクトで使ってみる

使い方は理解できたので、もう少し実用的な例で考えてみる。
今回はfirebase-admin/firestore にあるFirebaseの型定義を用いる。

firestoreではQueryDocumentSnapshotDocumentSnapshotという2つの型があり型定義を見ると前者は後者をextendsしている。

違いとしてはこれらのsnapshotから実際のデータを取り出すときにsnap.data()としてDocumentDataを得るのだが、この返り値がQueryDocumentSnapshotの場合はundefinedは含まれないがDocumentSnapshotの場合はundefinedが含まれる。

// QueryDocumentSnapshotの場合はundefinedが含まれない
function getData(snap: QueryDocumentSnapshot): DocumentData {
  return snap.data()
}

// DocumentSnapshotの場合はundefiendが含まれる
function getData(snap: DocumentSnapshot): DocumentData | undefined {
  return snap.data()
}

これを関数のオーバーロードを使って1つにまとめてみる。

function getData(snap: QueryDocumentSnapshot): DocumentData
function getData(snap: DocumentSnapshot): DocumentData | undefined
function getData(snap: DocumentSnapshot): DocumentData | undefined {
  return snap.data()
}

QueryDocumentSnapshotDocumentSnapshotをextendsしているものなので、実装部分はDocumentSnapshotにしておけばオーバーロードでQueryDocumentSnapshotを書いてもエラーにはならない。

これだけだとあまり嬉しくないのでここに返り値のデータにジェネリクスの型を返すような実装にしてみる。

function getData<T>(snap: QueryDocumentSnapshot): T
function getData<T>(snap: DocumentSnapshot): T | undefined
function getData(snap: DocumentSnapshot): unknown {
  return snap.data()
}

実装部分の返り値の型をunknownにして、オーバーロード部分に型パラメータを追加してそれを返り値に設定するようにした。
このように実装しておけば実際にこの関数を使うときに型パラメータを渡してやることで返り値の型がそれに対応するようになる。

// resultの型は以下のようになる
// snapがQueryDocumentSnapshotの場合はUser
// snapがDocumentSnapshotの場合はUser | undefined
const result = getData<User>(snap)

厳密には違うのかもしれないが、ここでやったオーバーロードは実質下記のような型アサーションと同じようなことをやっているという認識でいる。

function getData<T>(snap: QueryDocumentSnapshot): T {
  return snap.data() as unknown as T
}

実装部分とインターフェース部分の分離

先程のオーバーロードでまとめた部分で2行目と3行目は同じ型を書いている。
最初2行目は不要な気もしたが実際には必要である。

function getData(snap: QueryDocumentSnapshot): DocumentData
function getData(snap: DocumentSnapshot): DocumentData | undefined
function getData(snap: DocumentSnapshot): DocumentData | undefined {
  return snap.data()
}

2行目をコメントアウトして引数にDocumentSnapshotを与えるとエラーになったことから、3行目はあくまでも実装とその実装内容を満たす型定義とする必要があって、オーバーロードした場合は使う側ではオーバーロードされた型定義に従うという認識でいる。

表現として適切かは分からないが、関数のオーバーロードをした場合、実装部分とインターフェース部分が分かれてオーバーロード部分がインターフェースの定義としての役割を担う的なイメージでいる。

オーバーロードの適用順序

オーバーロードの適用順序は上から順に適用される。
つまり、先程のオーバーロードの1行目と2行目を入れ替えて、QueryDocumentSnapshotの引数を与えても1行目のオーバーロードが適用されてしまう。
(QueryDocumentSnapshotDocumentSnapshotをextendsしているので1行目に当てはまってしまう)

function getData(snap: DocumentSnapshot): DocumentData | undefined
function getData(snap: QueryDocumentSnapshot): DocumentData
function getData(snap: DocumentSnapshot): DocumentData | undefined {
  return snap.data()
}

// snapがQueryDocumentSnapshotだとしても返り値はDocumentData | undefinedになる
const result = getData(snap)

まとめ

存在は知っていてもそれを実際に有意義に使えるかとなると使えないものも多い。

関数のオーバーロードもその1つだったが、今回でちゃんと使い方と注意すべき点を理解できたと思う。

あまり乱用すべきではないと思うし使い所も多くはないかなと思うが、ライブラリの実装ではよく使われる気がしているので今後見る際に正しく読み取れそう。いい勉強になった。

この記事が参考になったからコーヒーくらいおごってもいいぜという方は、以下からサポートいただけると次の記事書くモチベになりますのでよろしくお願いします

Buy Me A Coffee

参考

あわせて読みたい
Documentation - More on FunctionsLearn about how Functions work in TypeScript.

この記事を書いた人

コメント

コメントする

目次
閉じる