TypeScript には、 Template Literal Types というものがあり、例えば、以下のような型を表現することが出来ます。
type Pixel = `${number}px`; const a: Pixel = "14px"; // valid const b: Pixel = "20pt"; // invalid
これで、色を表現してみましょう。
といっても、こんな感じで出来ます。
type HexDigit = | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "a" | "b" | "c" | "d" | "e" | "f" | "A" | "B" | "C" | "D" | "E" | "F"; type ColorHex<T extends string> = T extends `#${HexDigit}${HexDigit}${HexDigit}${infer Rest}` ? Rest extends `` ? T : Rest extends `${HexDigit}${HexDigit}${HexDigit}` ? T : never : never; type ColorRGB<T extends string> = T extends `rgb(${number},${number},${number})` ? T : never; type ColorRGBA<T extends string> = T extends `rgba(${number},${number},${number},${number})` ? T : never; type Color = string & { __type: "Color" }; const color = <T extends string>( w: ColorHex<T> | ColorRGB<T> | ColorRGBA<T> ): Color => { return w as string as Color; }; // valid const c0: Color = color("rgb(1, 1, 1)"); const c1: Color = color("rgba(1, 1, 1, 1)"); const c2: Color = color("#12345a"); // invalid const c3: Color = "#12345a"; const c4: Color = color("#fffa"); const c5: Color = color("#zzz");
ColorHex<T>
型は、単純に #xxx
もしくは #xxxxxx
をパースします。
ただし、 type ColorHex<T extends string> = T extends `#${HexDigit}${HexDigit}${HexDigit}${HexDigit}${HexDigit}${HexDigit}` : never;
とは出来ません。
というのも、そのようにした場合、 TypeScript コンパイラの制限に引っかかって、型エラーが発生します。
ちなみにエラー内容は Expression produces a union type that is too complex to represent.(2590)
で、多すぎるぞ!って感じですね。
なので、第 4 引数(?)以降は infer
で受け取って、再度 infer
した部分が要件を満たすかどうかをチェックすれば良いです。
残りの 2 つ、ColorRGB
と ColorRGBA
は、単純な Template Literal Types なので、特に解説は必要ないはずです。
あとは、これらを color
関数の引数として扱い、受け取りたい側が Color
を型指定してあげれば、型レベルでバリデーションが行えます。
ということで、メモでした。