JavaScript
TypeScript

TypeScript 2.5.3 での疑似文字列 enum の書き方

あれこれ試した結果、 const インスタンスを作って Mapped Types で型付けする方法に落ち着きました。

export type StringEnumAdapter<T> = { readonly [K in keyof T]: keyof T };

const FooKeyRaw = {
  ITEM_1: 'ITEM_1',
  ITEM_2: 'ITEM_2',
  ITEM_3: 'ITEM_31', // こういうミスは通ってしまう
};
export type FooKeyType = keyof typeof FooKeyRaw;
export const FooKey = FooKeyRaw as StringEnumAdapter<typeof FooKeyRaw>;

// これは (TypeScript 2.5.3 では) NG
const aKey: FooKeyType = FooKeyRaw.ITEM_1;

// これは OK
const bKey: FooKeyType = FooKey.ITEM_1;
console.log(bKey);

// これは NG
const cKey: FooKeyType = FooKey.NOT_EXIST;

// これも NG
FooKey.ITEM_1 = 'ITEM_2';

// ITEM_31
console.log(FooKey.ITEM_3);

その他の方法

namespace + export const

TypeScriptでのイベント名を管理・指定するもう一つの方法(+ストリングリテラル型にも対応)
https://qiita.com/ConquestArrow/items/02826db3ddbe98d280bd

class の static メンバ

TypeScriptでのイベント名をclickで指定するのはもう辞めよう
https://qiita.com/tonkotsuboy_com/items/b32801246ec14f444514

const でキーだけ宣言して関数で初期化

typo 等でキーと値が乖離するミスは防げます。が、2017-10-15 現在、uglifyjs-webpack-plugin を通してもコンパイル時解決されないので実行コストが...

declare global {
  interface ObjectConstructor {
    keys<T>(t: T): Array<keyof T>;
  }
}

export type StringEnumAdapter<T> = { readonly [K in keyof T]: keyof T };
type StringEnumAdapterMutable<T> = { [K in keyof T]: keyof T };

export function asStringEnum<T>(t: T): StringEnumAdapter<T> {
  const tt = t as any as StringEnumAdapterMutable<T>;
  for (const k of Object.keys(tt)) {
    tt[k] = k;
  }
  return tt;
}

const BarKeyRaw = {
  BAZ: null,
  CUX: null,
};
export type BarKeyType = keyof typeof BarKeyRaw;
export const BarKey = asStringEnum(BarKeyRaw);

// これは OK
const aaKey: BarKeyType = BarKey.BAZ;

// BAZ
console.log(aaKey);

// これはNG
const bbKey: BarKeyType = 'BAZOOKA';

// これもNG
BarKey.BAZ = BarKey.CUX;