本稿では、タプル型を用いて、TypeScriptのオーバーロード関数の可読性を少しだけ良くする書き方を紹介します。
他の言語と書き味が違う…
TypeScriptのオーバーロード関数は、Javaなどのオーバーロードとは仕組みがかなり異なっています。Javaなどでのオーバーロードは、インターフェイスごとに実装を書くようになっています。
// Kotlinのオーバーロードの例。
// よくある言語のオーバーロードは、インターフェイスと実装がひとまとまりになる書き方だが……
class Twicer {
fun twice(num: Int): Int {
return num * 2
}
fun twice(str: String): String {
return str + str
}
fun <T>twice(arr: Array<T>): Array<T> {
return arr + arr
}
}
一方、TypeScriptではオーバーロードは、インターフェイスは別々に定義できるのですが、実装部分はすべてのインターフェイスで共有する書き方になります。
class Twicer {
// インターフェイス
twice(num: number): number; // ❶
twice(str: string): string; // ❷
twice<T>(arr: T[]): T[]; // ❸
// 実装
twice<T>(value: number | string | T[]): number | string | T[] {
// インターフェイス❶に対応した実装
if (typeof value === "number") {
return value * 2;
}
// インターフェイス❷に対応した実装
if (typeof value === "string") {
return value + value;
}
// インターフェイス❸に対応した実装
return [...value, ...value];
}
}
TypeScriptからJavaScriptへのコンパイルというのは、雑な説明になりますが、TypeScriptのコードから型に関する部分を消すだけの処理です1。上のTypeScriptコードをコンパイルすると次のようなJavaScriptになります。
// JavaScriptコード
class Twicer {
twice(value) {
if (typeof value === "number") {
return value * 2;
}
if (typeof value === "string") {
return value + value;
}
return [...value, ...value];
}
}
どうせトランスパイルするなら、Javaのようなオーバーロード構文をTypeScriptでは実装しておいてほしいという意見もありますが、TypeScriptはJavaScriptの仕様を逸脱しない程度に型に関する構文を追加することを方針にしている2ので、JavaScriptにJavaのようなオーバーロード構文がない以上、いたしかたないことです。
オーバーロードは実装がごちゃつく
いたしかたないのですが、インターフェイスによっては、実装がかなりごちゃついてしまいます。
たとえば、次のようなインターフェイスでは、
// 当てられた日付が未来だったらtrue、そうでなければfalseを返す関数
function isFutureDate(year: number, month: number, day: number): boolean;
function isFutureDate(date: Date): boolean;
function isFutureDate(isoDateString: string): boolean;
実装は、次のようになります。
// 実装
function isFutureDate(
yearOrDateOrIsoDateString: number | Date | string,
month?: number,
day?: number
): boolean {
if (
typeof yearOrDateOrIsoDateString === "number" &&
typeof month === "number" &&
typeof day === "number"
) {
return new Date(yearOrDateOrIsoDateString, month, day) > new Date();
}
if (typeof yearOrDateOrIsoDateString === "string") {
return new Date(yearOrDateOrIsoDateString) > new Date();
}
return yearOrDateOrIsoDateString > new Date();
}
可読性はあまり良くありません。微修正できる余地はありそうですが、限度はありそうです。
可読性が悪いと感じられる部分
上の例では、引数ごとにユニオン型を使ったり↓
yearOrDateOrIsoDateString: number | Date | string,
オプション型を使ったり↓
month?: number,
day?: number
していました。
これが可読性を損ねているひとつのポイントです。たとえば、number | Date | string
のどの型が何番目のインターフェイスに対応しているのかぱっとわかりにくい点があります。
次に、第2引数のnumber
は、第1引数のnumber | Date | string
のどれとセットなのかが、インターフェイスを参照しながらじゃないとつかみにくいというのもあります。
タプル型でオーバーロード関数の可読性をちょっとだけ高くする
上の問題点を改善できるのが、タプルを使った書き方です。次のように書きます。
// インターフェイス
function isFutureDate(year: number, month: number, day: number): boolean;
function isFutureDate(date: Date): boolean;
function isFutureDate(isoDateString: string): boolean;
// 実装
function isFutureDate(
...args:
| [year: number, month: number, day: number]
| [date: Date]
| [isoDateString: string]
): boolean { /*...*/ }
上の実装部分をもともとの例↓と比べてみてください。
function isFutureDate(
yearOrDateOrIsoDateString: number | Date | string,
month?: number,
day?: number
): boolean { /*...*/ }
記述量はあまり変わってはいませんが、改善後のほうが実装の引数とインターフェイスの対応関係がより分かりやすくなっていませんでしょうか。
実装のロジックまで書くと次のようになります。
function isFutureDate(
...args:
| [year: number, month: number, day: number]
| [date: Date]
| [isoDateString: string]
): boolean {
if (args.length === 3) {
const [year, month, day] = args;
return new Date(year, month, day) > new Date();
}
const dateOrString = args[0];
if (dateOrString instanceof Date) {
return dateOrString > new Date();
}
return new Date(dateOrString) > new Date();
}
タプル型では、length
で型の絞り込みが行えるので、引数一個一個に対してtypeof
をかけるといった必要もなくなります。
おわり
タプル型を使うことで、もともと書きにくい&読みにくくなりがちなTypeScriptのオーバーロードをちょっとだけ良くする方法を紹介しました。
オーバーロード自体、あまりグッドパーツではないと思いますが、どうしても必要になったときにお役に立てていただければと思います。