enum vs ユニオン型、どちらを選ぶか

TypeScript で定数の集合を表現するとき、enum を使う方法とユニオン型を使う方法がある。どちらも似たような目的を果たせるが、挙動や生成されるコードはまったく異なる。それぞれの特性を理解した上で、適切に使い分けたい。

enum の正体

enum は TypeScript 独自の構文で、コンパイル後に JavaScript のオブジェクトとして残る。

enum Status {
  Pending,
  Approved,
  Rejected,
}

console.log(Status.Pending);  // 0
console.log(Status[0]);       // "Pending"(逆引き)

コンパイル後の JavaScript を見ると、その実体がわかる。

var Status;
(function (Status) {
    Status[Status["Pending"] = 0] = "Pending";
    Status[Status["Approved"] = 1] = "Approved";
    Status[Status["Rejected"] = 2] = "Rejected";
})(Status || (Status = {}));

数値から文字列への逆引きができるよう、双方向のマッピングが生成されている。便利な反面、バンドルサイズが増えるというデメリットがある。

ユニオン型のシンプルさ

一方、ユニオン型は純粋に型の世界だけで完結する。

type Status = "pending" | "approved" | "rejected";

const status: Status = "pending";

コンパイル後は型情報が消えるだけで、余計なコードは一切生成されない。

enum

ランタイムに実体が残る。逆引きや列挙が可能。バンドルサイズが増える。

ユニオン型

コンパイル時のみ存在。ゼロコスト。ただし値の列挙には別途配列が必要。

enum の落とし穴

数値 enum には、型安全性を損なう罠がある。

enum Status {
  Pending,
  Approved,
}

function process(status: Status) {
  console.log(status);
}

process(Status.Pending);  // OK
process(999);             // なぜか OK(型エラーにならない)

数値 enum は任意の数値を受け入れてしまう。これは TypeScript の設計上の歴史的経緯によるもので、ビットフラグ操作を許容するための仕様だ。文字列 enum であればこの問題は起きないが、ユニオン型のほうがより直感的に型安全を実現できる。

const enum という選択肢

バンドルサイズの問題を解決するために const enum も用意されている。

const enum Direction {
  Up,
  Down,
  Left,
  Right,
}

const d = Direction.Up;  // コンパイル後は const d = 0; になる

インライン展開されるため実行時のオーバーヘッドはないが、--isolatedModules オプションとの相性が悪く、一部のビルドツールでは使用が推奨されていない。

どちらを選ぶか

ユニオン型を選ぶ場面

シンプルな定数の集合を定義したいとき。バンドルサイズを気にするとき。React や Next.js など、モダンなフロントエンド開発。

enum を選ぶ場面

値の逆引きが必要なとき。ビットフラグを扱うとき。既存のコードベースが enum を使っているとき。

現代の TypeScript 開発では、ユニオン型 + as const の組み合わせが主流になりつつある。

const STATUS = {
  PENDING: "pending",
  APPROVED: "approved",
  REJECTED: "rejected",
} as const;

type Status = typeof STATUS[keyof typeof STATUS];
// "pending" | "approved" | "rejected"

この方法なら、オブジェクトとして値を参照しつつ、型としても使える。enum の利便性とユニオン型の型安全性を両立できるアプローチだ。