Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

String Enum に重複がないことを静的型検査で保証する

数値の Enum であればかんたんに重複なく生成できます。

// 0始まりの連番を生成
const [ITEM1, ITEM2, ITEM3] = Array(10).keys();

(この例では ITEM1 などが number になってしまっています。最後にいい感じに型付けする方法をおまけで紹介しています。)

しかし、String Enum の場合、重複がないようにするのが難しいです。

const STATE1 = `prefix_state1`,
  STATE2 = `prefix_state2`,
  STATE3 = `prefix_state1`;
// コピペして作ったので STATE3 が prefix_state1 になっている!!

こういう事故を防ぐために、型チェックで弾きたいですよね?

String Enum で同じ値がないことを型検査で保証する

createStringEnum という型チェックのための恒等関数を作りました。(引数が2つあるので厳密には恒等関数ではないですが)

const createStringEnum = <T extends object, Prefix extends string>(
  _prefix: Prefix,
  obj: {
    [K in keyof T]: K extends string ? `${Prefix}_${Lowercase<K>}` : never;
  }
) => obj;

createStringEnum 関数を使えば、型チェックで重複がないと保証された String Enum を生成できます。

const { STATE1, STATE2, STATE3 } = createStringEnum('prefix', {
  STATE1: `prefix_state1`,
  STATE2: `prefix_state2`,
  STATE3: `prefix_state3`,
} as const);

もし先ほどのように STATE3 が prefix_state1 になっていて重複がある場合、 createStringEnum 関数は型エラーを起こします。よって、STATE1、STATE2、STATE3 がそれぞれ異なっていることが型的に保証されました。

おまけ: 数値の Enum でもちゃんと型的に異なる値を作りたい

下の例では ITEM1 などが number 型となっていました。

const [ITEM1, ITEM2, ITEM3] = Array(10).keys();

しかし、Enum として使いたいので number ではなく、0 や 1 などリテラル型を振りたいですよね?

そう思い、Sequence 型を作りました。

type Sequence<
  N extends number,
  Acc extends number[] = []
> = Acc['length'] extends N ? Acc : Sequence<N, [...Acc, Acc['length']]>;

const createSequence = <N extends number>(n: N): Sequence<N> =>
  Array.from(Array(n).keys()) as Sequence<N>;

const [ITEM1, ITEM2, ITEM3] = createSequence(3);

これで ITEM1 は 0、ITEM2 は 1、ITEM3 は 2 というふうに型がつけられます。

(Assertion を使っているので完全に型安全というわけではないですが、さすがに Array(n).keys() というコードで連番を生成できることは静的型検査で保証せずとも使ってよいと思います。)