なつねこメモ

主にプログラミング関連のメモ帳 ♪(✿╹ヮ╹)ノ 書いてあるコードは自己責任でご自由にどうぞ。記事本文の無断転載は禁止です。

TypeScript で色を表現したい

TypeScript には、 Template Literal Types というものがあり、例えば、以下のような型を表現することが出来ます。

type Pixel = `${number}px`;

const a: Pixel = "14px"; // valid
const b: Pixel = "20pt"; // invalid

TypeScript Playground

これで、色を表現してみましょう。
といっても、こんな感じで出来ます。

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");

TypeScript Playground

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 つ、ColorRGBColorRGBA は、単純な Template Literal Types なので、特に解説は必要ないはずです。
あとは、これらを color 関数の引数として扱い、受け取りたい側が Color を型指定してあげれば、型レベルでバリデーションが行えます。

ということで、メモでした。