【無料配布】10日間で学べるTypeScript学習資料

【TypeScript】Enumの役割と実務の使い分け|Union Typesとの比較

Enum(列挙型)は、関連する定数のセットを一つの名前空間にまとめ、意味を持たせるための強力な機能です。

JavaScriptには標準で存在しない機能であり、TypeScriptが導入した数少ない「ランタイムに影響を与える機能」の一つです。

例えば、アプリケーション内で「ユーザーの役割(管理者/編集者/一般)」や「注文ステータス(待機中/発送済み/完了)」など、決まった選択肢の中から値を扱う場面があります。

文字列や数値で管理するのではなく、Enumとして定義することで、コードの可読性が飛躍的に向上し、意図しない値の混入を防ぐことができます。

目次

Enumの基本:数値列挙型と文字列列挙型

TypeScriptにおいてEnumを定義する際、主に「数値」をベースにするか、「文字列」をベースにするかの二択を迫られます。

これは将来のデバッグのしやすさやシステム全体の堅牢性を大きく左右します。

Enumの基本
  • 数値列挙型の挙動と初期化
  • 文字列列挙型のメリットとデバッグ効率
  • 異種混合列挙型の注意点

数値列挙型の挙動と初期化

数値列挙型は、TypeScriptのEnumにおいて自動化の恩恵を強く受ける形式です。

最大の特徴は、「自動インクリメント(連番付与)」にあります。

初期値を指定しない場合、TypeScriptは最初のメンバーに 0 を割り当て、以降のメンバーに 1, 2, 3… と順番に値を振っていきます。

enum ResponseStatus {
  Success, // 0
  Failure, // 1
  Unknown, // 2
}

特定の数値から連番を開始したい場合は、最初のメンバーに数値を代入します。

enum HttpStatus {
  OK = 200,
  Created = 201,
  Accepted = 202,
  BadRequest = 400,
  Unauthorized, // 401 (自動計算)
}

数値列挙型には、TypeScriptエンジニアが絶対に知っておくべき「型安全性の穴」があります。

注意点として「Enumで定義されていない範囲の数値」であっても、変数に代入できてしまう点は気を付けましょう。

文字列列挙型のメリットとデバッグ効率

文字列列挙型はTypeScript 2.4で導入されて以来、実務のスタンダードとなっています。

数値列挙型とは異なり、各メンバーに意味のある文字列を明示的に割り当てます。

enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

console.log(Direction.Up); // 出力: "UP" (数値型なら 0)

文字列列挙型には、数値型にある「逆引き(値から名前を引く機能)」が存在しません。

しかし、実行時のオーバーヘッドを小さく抑えられるといったメリットでもあります。

異種混合列挙型の注意点

TypeScriptの文法上、数値と文字列を一つのEnum内に混在させることが可能です。

これを「異種混合列挙型」と呼びます。

enum BooleanLikeHeterogeneousEnum {
  No = 0,
  Yes = "YES",
}

ただし、異種混合列挙型は以下のようなリスクを伴います。

異種混合列挙型のリスク
  • 可読性の低下
  • メンテナンスの困難さ
  • 型推論の複雑化

異種混合列挙型は、JavaScriptの古いライブラリをラップする場合などの特殊な事情がない限り、避けるのが賢明です。

計算されたメンバーと定数メンバー

Enumのメンバーは、大きく分けて「定数メンバー」と「計算されたメンバー」の2種類に分類されます。

スクロールできます
特徴定数メンバー計算されたメンバー
評価タイミングコンパイル時実行時 (Runtime)
値の指定リテラル、定数式、参照関数呼び出し、動的プロパティ
自動補完後続のメンバーに連番を振れる後続のメンバーには明示的な初期化が必要
主な用途状態、方向、エラーコードなど設定値、動的なID、環境依存の値

定数メンバーとは、TypeScriptのコンパイル時にその値が確定しているメンバーのことです。

一方で、計算されたメンバーは、実行時(Runtime)にならないと最終的な値が決まらないメンバーです。

enum FileAccess {
  // すべて定数メンバー
  None,
  Read    = 1 << 1,
  Write   = 1 << 2,
  ReadWrite = Read | Write,
}
enum Logic {
  // 計算されたメンバー
  Alpha = "typescript".length,
  Beta  = Math.random(),
  Gamma = getInitialValue(),
}

function getInitialValue() {
  return 42;
}

TypeScriptのEnumが生成するJavaScriptコード

TypeScriptの魅力は、コンパイル(トランスパイル)後に型定義が消え去り、クリーンなJavaScriptが残る点にあります。

しかし、Enumはこのルールの「例外」です。

EnumはJavaScriptのオブジェクトとして実体が残ります。

逆引きマッピングの仕組み

TypeScriptは「逆引きマッピング」という特殊なオブジェクト構造を生成します。

「名前から値」を引けるだけでなく、「値から名前」も引けるようにする工夫です。

enum Color {
  Red = 0,
}

トランスパイルされると、以下のようなJavaScriptコードになります。

"use strict";
var Color;
(function (Color) {
    Color[Color["Red"] = 0] = "Red";
})(Color || (Color = {}));

Color[Color["Red"] = 0] = "Red" というコードが、逆引きマッピングです。

Color["Red"] = 0Color[0] = "Red"の二重代入により、最終的なオブジェクトは以下のようになります。

{
  "Red": 0,
  "0": "Red"
}

逆引きマッピングの最大の利点は、ログ出力やデバッグの際の見やすさにあります。

const enum(定数列挙型)とパフォーマンス

通常Enumが「型かつ実体を持つ」という二面性に対し、const enum はパフォーマンスと効率を追求したEnumです。

スクロールできます
特徴const enum通常のEnum
JS出力なし(値が埋め込まれる)オブジェクトとして出力される
実行時メモリゼロオブジェクト分を消費
isolatedModules非推奨 / エラーの原因安全
逆引き不可可能(数値型のみ)

トランスパイル後の挙動とツリーシェイキングへの影響

const enumの特徴は、「コンパイル後に消え去る」という点にあります。

const enum は、その参照箇所がすべて直接の値(リテラル)に置き換わります(インライン展開)

// TypeScript
const enum LogLevel {
  Debug,
  Info,
}

const currentLevel = LogLevel.Debug;

コンパイルされると、JavaScriptは以下のようになります。

// JavaScript
const currentLevel = 0; // LogLevel.Debug の実体に置き換わる

オブジェクトを定義するための即時実行関数(IIFE)すら生成されません。

isolatedModules フラグ下での注意点

Vite、esbuild、SWCといった高速なビルドツールを使用している環境では、致命的な弱点があります。

tsconfig.jsonisolatedModules フラグとの衝突です。

モダンなビルド環境(Viteなど)では、const enum を避けるのが一般的です。

以下のいずれかを選択します。

ベストプラクティス
  • 通常のEnum
  • as const を使ったオブジェクト
const LogLevel = {
  Debug: 0,
  Info: 1,
} as const;
type LogLevel = typeof LogLevel[keyof typeof LogLevel];

const enum と同等の型安全性、JavaScript挙動の予測しやすさを両立します。

Enum vs Union Types vs Object Literal

TypeScriptにはenum以外にも、JavaScriptのオブジェクトを活用したas const(Object Literal)や、型システムを駆使したUnion Typesといった代替手段が存在します。

多くの開発者がEnumを避けるのか?

Googleの「TypeScript Style Guide」をはじめ、多くのテック企業の規約ではEnumの使用を制限、あるいは完全に禁止しています。

これには大きく分けて3つの理由があります。

Enumを避ける理由
  • TypeScriptの設計思想との乖離
  • ツリーシェイキングの妨げ
  • 実行時の「謎の挙動」

Enumはコンパイル後も即時実行関数(IIFE)やオブジェクトとして実体が残ります。

これはTypeScriptのアイデンティティに対する「不純物」と捉えられることがあります。

また、Enumの定義が製品コード(bundleファイル)に含まれ続け、ファイルサイズを大きくする原因になります。

さらに、直感に反する挙動があり、「予期せぬバグの温床」として嫌われる要因です。

どれを選ぶべきか?の判断基準

結論として、以下のパターンで考えておきましょう。

判断基準
  • 基本「Union Types」を選ぶ
  • 名前空間が欲しいならObject + as constを選ぶ
  • 「逆引き必須」または「歴史的経緯」があるなら「Enum」を選ぶ

迷った時は、Union Typesで表現できないかを考え、必要に応じてas constオブジェクトに格上げするというステップを踏むのが最も安全な道と言えるでしょう。

実務で役立つEnumのテクニック

Enumは定数を並べるだけではありません。

型システムと組み合わせることで、「型としての側面」と「値としての側面」を自在に行き来できるようになります。

Enumのキーや値の型抽出

Enumを定義すると、暗黙的に「型」と「値」の両方として振る舞います。

Enumの名前そのものを型として使うと、自動的に「Enumに含まれる値のUnion型」になります。

enum UserRole {
  Admin = "ADMIN",
  Editor = "EDITOR",
  Guest = "GUEST",
}

// roleは "ADMIN" | "EDITOR" | "GUEST" の型になる
const checkAccess = (role: UserRole) => { ... };

また、Enumのプロパティ名を文字列のUnion型として取得するには、keyof typeof を組み合わせます。

// "Admin" | "Editor" | "Guest" というUnion型が作れる
type UserRoleKey = keyof typeof UserRole;

const roleKey: UserRoleKey = "Admin"; // OK
// const roleKey: UserRoleKey = "ADMIN"; // エラー(値の方はダメ)

網羅性チェックへの応用

実務で価値があるテクニックが、「網羅性チェック」です。

switch文やif文で、Enumのすべてのケースを漏れなく処理しているかをコンパイラにチェックさせる手法です。

新しいEnumのメンバーを追加したときに、処理を書き忘れてバグを出す…というミスを物理的に防げます。

enum TaskStatus {
  Todo,
  InProgress,
  Done,
  // 開発中、ここに 'Archived' を後から追加したとする
}

function getStatusLabel(status: TaskStatus): string {
  switch (status) {
    case TaskStatus.Todo:
      return "これからやる";
    case TaskStatus.InProgress:
      return "やってる";
    case TaskStatus.Done:
      return "終わった";
    default:
      // ここがポイント!
      const _exhaustiveCheck: never = status;
      return _exhaustiveCheck;
  }
}

このエラーのおかげで、実行前に気づくことができます。

大規模なシステムをリファクタリングする際に有用です。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次