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 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 を使っているとき。
現代の TypeScript 開発では、ユニオン型 + as const の組み合わせが主流になりつつある。
const STATUS = {
PENDING: "pending",
APPROVED: "approved",
REJECTED: "rejected",
} as const;
type Status = typeof STATUS[keyof typeof STATUS];
// "pending" | "approved" | "rejected"この方法なら、オブジェクトとして値を参照しつつ、型としても使える。enum の利便性とユニオン型の型安全性を両立できるアプローチだ。