はじめに
2022-08-30
のような文字列を /\d\d\d\d-\d\d-\d\d/
という正規表現を満たす型として表現出来ないかで悩んでいたところ、同僚から TypeScript: Type Safe Date Strings という記事を教えてもらったので、この記事を参考に型安全な日付型を実装してみました。
日付を構成する要素を型で表現する
日付は、年
, 月
, 日
という要素に分解できるので、まずはこれらを型で表現することを試みます。
年
, 月
, 日
型
TypeScript は Template Literal Types をサポートしており、${}
で リテラルのUnion型を展開することが出来ます。
年
は上位2桁について(200年に限定すると) 19 | 20
、 下位2桁は 00 ~ 99
、それぞれの桁は 0-9
であるため、Template Literal Types を使い以下のように定義出来ます。
type d = 1|2|3|4|5|6|7|8|9|0;
type YYYY = `19${d}${d}` | `20${d}${d}`;
年
型とほぼ同様に、月
、 日
型も以下のように表現できます。
type oneToNine = 1|2|3|4|5|6|7|8|9;
type MM = `0${oneToNine}` | `1${0|1|2}`;
type DD = `${0}${oneToNine}` | `${1|2}${d}` | `3${0|1}`;
Day型を作る
Date型はオチがあるので、まずは Day型を作ります。
class Day {
static isDayString(day: string): day is DD {
return !!day.match(/\d\d/)
}
private _day: DD | undefined
constructor(day: string) {
if (Day.isDayString(day)) {
this._day = day
}
}
}
Type Guard を使い、day: string
を DD
型に絞り込みますが、DD
型は Template Literal Types で展開され、 01
~ 31
までのリテラルのUnion型が定義されました。
筆者のVSCode上でも展開された状態で表現されています。
Date型を作る
ここまで来たらオチがわかったかもしれませんが、ひとまずDay型と同様にDate型を作ってみます。
export type DateYMDString = `${YYYY}-${MM}-${DD}`;
export class Date {
static isDateYMDString(date: string): date is DateYMDString {
return !!date.match(/\d\d\d\d-\d\d-\d\d/)
}
private _date: DateYMDString | undefined
constructor(date: string) {
if (Date.isDateYMDString(date)) {
this._date = date
}
}
}
上記のコードでコンパイルなどは通ると思いますが、IDE上で入力すると、突然CPUが唸り声をあげるかもしれません。
IDE上ではDay型と同様に Template Literal Types で 1900-01-01
~ 2099-12-31
まで、 75000近くのリテラル型が展開されました。
実用上使えるか
常識的な感覚だと、75000近くのリテラル型が展開されるのは多く感じますし、TypeScript側でも 100'000 までしかサポートされていないということです。
1900-01-01
~ 2099-12-31
まで200年近くしか対応できていないのに、上限に近づいているということで、 Template Literal Types で型安全な日付型を作るのは、実用的な面でいうと厳しいと感じました。
また、別の方法などありましたら、コメントいただけると幸いです。