🍈

[v4系]TypeScriptでenumを実現する

2023/07/25に公開

TypeScriptでenum(列挙型)は積極的には使わない方が良い、と調べるとしばしば出てきます。「自分でもまとめよう」と思いplaygroundで試したところ、なんと出るはずのコンパイルエラーが発生しません。どうやらv5のアップデートで(一部?)対応が入ったようです。

とはいえ、まだまだv4系を使っているサービスもたくさんあると思います。v4系で動作確認したものをまとめようと思います。

動作確認環境

TypeScriptのenum

TypeScriptでは文字ベースのenum=文字列列挙型、と数値ベースのenum=数値列挙型を定義して使うことができます。

enum Direction {
  Top = 0,
  Right = 1,
  Bottom = 2,
  Left = 3
}

const canMove = (direction: Direction) => {
  if(direction === Direction.Top) {
    return true;
  }
  return false;
}

console.log(canMove(Direction.Bottom)); // false

TypeScriptのenumの懸念点

数値列挙型には型安全上の問題点があります。

以下のように、0から3の値しか持たないはずの数値列挙型に、それ以外の値を入れてもコンパイルエラーが発生しません。

enum Direction {
  Top = 0,
  Right = 1,
  Bottom = 2,
  Left = 3
}

const direction: Direction = 10;
console.log(direction); // 10

よって、関数の引数の型として数値列挙型を指定してたとしても、コンパイルエラーなしで列挙型で定義した値以外を引数にとることができます。

enum Direction {
  Top = 0,
  Right = 1,
  Bottom = 2,
  Left = 3
}

const canMove = (direction: Direction) => {
  if(direction === Direction.Top) {
    return true;
  }
  return false;
}

console.log(canMove(10)); // false

また、列挙型に値を指定することでメンバー名を得ることもできますが、その時も列挙型に存在しない値を指定してもコンパイルエラーが発生しません。

enum Direction {
  Top = 0,
  Right = 1,
  Bottom = 2,
  Left = 3
}

console.log(Direction[0]); // "Top"
console.log(Direction[10]); // undefined

TypeScriptのenumの代替案

Union型

シンプルな代替案です。以下のようにenumの代わりにUnion型を使います。

type Direction = "Top" | "Right" | "Bottom" | "Left";

const canMove = (direction: Direction) => {
  if(direction === "Top") {
    return true;
  }
  return false;
}

console.log(canMove("Top")); // true

canMove 関数の中のif文で direction === "Top" としています。ここでDirectionに定義している値以外を入れようとするとコンパイルエラーが発生します。
canMove関数呼び出し時の引数の指定も同様です。

型安全という点では良さそうです。ですがDirectionを別ファイルでも使いたい、などのように使う範囲が広がってくると、直接String値を使わなければならない部分が個人的にはすごく気になります。

オブジェクトリテラル

オブジェクトリテラルを使うことで、直接Stringの値を使う必要がなくなります。

const Direction = {
  Top: 0,
  Right: 1,
  Bottom: 2,
  Left: 3,
} as const;
type Direction = typeof Direction[keyof typeof Direction];

const canMove = (direction: Direction) => {
  if(direction === Direction.Top) {
    return true;
  }
  return false;
}

console.log(canMove(Direction.Top)); // true

少し詳細に説明をします。

const Direction = {
  Top: 0,
  Right: 1,
  Bottom: 2,
  Left: 3,
} as const;

as constをつけることで、オブジェクトのプロパティがreadonlyになり、リテラルタイプで指定した値と同等とみなされます。

as constあり

as constなし

as constなしの方は、各プロパティにreadonlyが付いておらず、型もnumberと推測されています。一方、as constありの方は、各プロパティにreadonlyが付き、型がそれぞれに指定したリテラル値となっています。
as constにすることによってプロパティをreadonlyにすることができるため、プロパティの値を書き換えられることを防げます。

続いて、以下のコードによって、結果としてtype Directionは 0 | 1 | 2 | 3 のUnion型となります。

type Direction = typeof Direction[keyof typeof Direction];
// type Direction = 0 | 1 | 2 | 3

コードの右側から解説です。

type DirectionType = typeof Direction;
// type DirectionType = {
//     readonly Top: 0;
//     readonly Right: 1;
//     readonly Bottom: 2;
//     readonly Left: 3;
// }

type KeyofType = keyof DirectionType;
// type KeyofType = "Top" | "Right" | "Bottom" | "Left"

type IndexedAccessType = DirectionType[KeyofType];
// type IndexedAccessType = 0 | 1 | 2 | 3

利用している演算子等々は以下が参考になるかと思います。

オブジェクトリテラルを利用することによって、Union型を利用しつつ、Direction.Topのようにアクセスすることも可能になります。

参考

Discussion