T-CREATOR

<div />

TypeScriptのenumとunion型を比較・検証する どちらを選ぶべきか判断基準を整理

2026年1月19日
TypeScriptのenumとunion型を比較・検証する どちらを選ぶべきか判断基準を整理

TypeScript で定数の集合を表現する際、enum と union 型のどちらを選ぶべきか迷う場面は多いです。本記事では、実行時表現の有無・拡張性・型安全性の観点から両者を比較し、実務での判断基準を整理します。静的型付けを活かした設計を検討している方、チーム開発でコーディング規約を策定したい方に向けて、検証結果と実体験をもとに解説します。

enum と union 型の比較

観点enumunion 型
実行時表現JavaScript オブジェクトとして残るコンパイル後に消える
バンドルサイズやや増加する影響なし
実行時の値検証Object.values() で可能別途配列の定義が必要
型推論の精度抽象的な型として推論されるリテラル型として推論される
IDE リファクタリング参照元の一括変更が容易やや手間がかかる
ライブラリ連携型変換が必要な場合あり多くのライブラリと親和性が高い
拡張性宣言マージで拡張可能(数値 enum のみ)型エイリアスの再定義は不可

それぞれの詳細は後述します。

検証環境

  • OS: macOS Sequoia 15.2
  • Node.js: 24.1.0(LTS)
  • TypeScript: 5.9.3
  • 主要パッケージ:
    • ts-node: 10.9.2
  • 検証日: 2026 年 01 月 19 日

enum と union 型が混在しやすい背景

TypeScript では定数の集合を表現する方法が複数あります。このセクションでは、なぜ enum と union 型の選択が議論になるのか、その背景を整理します。

JavaScript には enum がない事実

TypeScript の enum は、JavaScript に存在しない構文を追加したものです。一方、union 型はあくまで型システム上の概念であり、JavaScript のコードには一切影響を与えません。

typescript// enum はコンパイル後もオブジェクトとして残る
enum Status {
  Draft = "draft",
  Published = "published",
}

// コンパイル後の JavaScript
var Status;
(function (Status) {
  Status["Draft"] = "draft";
  Status["Published"] = "published";
})(Status || (Status = {}));
typescript// union 型はコンパイル後に消える
type Status = "draft" | "published";

// コンパイル後の JavaScript
// (何も出力されない)

つまずきやすい点:enum が「JavaScript に変換される」という事実を知らずに使い始めると、バンドルサイズや Tree Shaking の問題に後から気づくことがあります。

TypeScript First ライブラリの増加

近年、Zod・React Hook Form・tRPC など、TypeScript を前提としたライブラリが増えています。これらの多くは union 型ベースの API を採用しており、enum との型変換が必要になるケースがあります。

typescriptimport { z } from "zod";

// Zod は union 型ベースの enum を提供
const StatusSchema = z.enum(["draft", "published", "archived"]);
type Status = z.infer<typeof StatusSchema>;
// 'draft' | 'published' | 'archived'

// TypeScript の enum を使うと型変換が必要
enum StatusEnum {
  Draft = "draft",
  Published = "published",
  Archived = "archived",
}

// Zod スキーマと enum の値を一致させるには工夫が必要
const statusValue: Status = StatusEnum.Draft; // 型エラーになる場合がある

実際に試したところ、TypeScript 5.9 では文字列 enum の値がリテラル型と互換性を持つため、多くのケースで型エラーは発生しませんでした。しかし、バージョンや設定によっては型アサーションが必要になります。

実行時表現の有無による違いと課題

enum と union 型の最も根本的な違いは、実行時に値として存在するかどうかです。この違いがもたらす具体的な課題を見ていきます。

enum の実行時コストとメリット

enum は JavaScript のオブジェクトとしてバンドルに含まれます。これはデメリットだけでなく、実行時の値検証に使えるというメリットもあります。

typescriptenum UserRole {
  Admin = "admin",
  Editor = "editor",
  Viewer = "viewer",
}

// 実行時に有効な値かどうかを検証できる
function isValidRole(value: string): value is UserRole {
  return Object.values(UserRole).includes(value as UserRole);
}

// API からの入力値を検証
const input = "admin";
if (isValidRole(input)) {
  console.log(`有効なロール: ${input}`);
}

下図は、enum と union 型のコンパイル結果の違いを示しています。

mermaidflowchart LR
  subgraph TS["TypeScript"]
    enumDef["enum UserRole"]
    unionDef["type UserRole"]
  end
  subgraph JS["JavaScript"]
    enumObj["UserRole オブジェクト"]
    nothing["(何も生成されない)"]
  end
  enumDef --> enumObj
  unionDef --> nothing

enum は実行時オブジェクトに変換され、union 型は型情報のみでランタイムには何も残りません。

union 型で実行時検証を行う方法

union 型は実行時に存在しないため、値の検証には別途配列を定義する必要があります。as const を活用することで、型と値の定義を一箇所にまとめられます。

typescript// 定数配列から union 型を導出するパターン
const USER_ROLES = ["admin", "editor", "viewer"] as const;
type UserRole = (typeof USER_ROLES)[number];
// 'admin' | 'editor' | 'viewer'

// 実行時の値検証
function isValidRole(value: string): value is UserRole {
  return (USER_ROLES as readonly string[]).includes(value);
}

検証の結果、このパターンは型安全性を保ちながら実行時検証も可能にする、union 型のベストプラクティスとして定着しています。

つまずきやすい点as const を忘れると、配列の型が string[] になり、union 型として導出できなくなります。

バンドルサイズへの影響

業務で React アプリケーションを開発していた際、enum を多用したコードで Tree Shaking が効かず、バンドルサイズが想定より大きくなった経験があります。

typescript// enum はすべてバンドルに含まれる
enum AllStatus {
  Draft = "draft",
  Published = "published",
  Archived = "archived",
  Pending = "pending",
  Rejected = "rejected",
}

// 使用しているのは Draft のみでも、全値がバンドルに含まれる
const status = AllStatus.Draft;

union 型であれば、使用していない値はそもそも JavaScript に存在しないため、この問題は発生しません。ただし、実際のバンドルサイズへの影響は数百バイト〜数キロバイト程度であり、大規模でなければ問題にならないことも多いです。

型安全性と静的型付けの観点での比較

このセクションでは、TypeScript の型システムとしての振る舞いの違いを掘り下げます。

型推論の精度

enum と union 型では、TypeScript が推論する型の粒度が異なります。

typescript// enum の場合
enum Color {
  Red = "red",
  Blue = "blue",
}

const colors = Object.values(Color);
const first = colors[0];
// first の型: Color(具体的な値は不明)

// union 型の場合
const COLORS = ["red", "blue"] as const;
type Color = (typeof COLORS)[number];

const first = COLORS[0];
// first の型: 'red'(具体的な値が推論される)

union 型はリテラル型として推論されるため、より精密な型チェックが可能です。

網羅性チェック(Exhaustiveness Check)

switch 文での網羅性チェックは、enum と union 型の両方で機能します。

typescripttype Status = "draft" | "published" | "archived";

function getStatusLabel(status: Status): string {
  switch (status) {
    case "draft":
      return "下書き";
    case "published":
      return "公開中";
    case "archived":
      return "アーカイブ済み";
    default:
      // status の型は never になる
      const _exhaustive: never = status;
      return _exhaustive;
  }
}

つまずきやすい点default 節で never 型を使った網羅性チェックを入れておくと、新しい値が追加されたときにコンパイルエラーで気づけます。

strictNullChecks との相性

TypeScript の null 安全を支える strictNullChecks は、enum と union 型の両方で正しく機能します。ただし、enum の数値型では意図しない挙動に注意が必要です。

typescript// 数値 enum の場合、任意の数値が代入できてしまう
enum NumericStatus {
  Active = 0,
  Inactive = 1,
}

const status: NumericStatus = 999; // エラーにならない!

// 文字列 enum や union 型ではこの問題は起きない
enum StringStatus {
  Active = "active",
  Inactive = "inactive",
}

const status2: StringStatus = "unknown"; // エラーになる

実際にこの問題で本番障害を起こした経験があり、以降は数値 enum を避けるようにしています。

設計上の判断基準と解決策

ここまでの比較を踏まえ、実務での設計判断の指針を整理します。

union 型を選ぶべきケース

以下のケースでは union 型が適しています。

ライブラリとの連携が重要な場合

typescriptimport { z } from "zod";
import { useForm } from "react-hook-form";

// Zod スキーマと連携
const formSchema = z.object({
  priority: z.enum(["low", "medium", "high"]),
});

type FormData = z.infer<typeof formSchema>;

// React Hook Form との連携も自然
const { register } = useForm<FormData>();

バンドルサイズを最小化したい場合

typescript// 型のみの定義でランタイムコストゼロ
type Theme = "light" | "dark" | "system";
type Language = "ja" | "en" | "ko";

Template Literal Types を活用する場合

typescripttype HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = "users" | "posts";

type ApiRoute = `${HttpMethod} /api/${Endpoint}`;
// 'GET /api/users' | 'GET /api/posts' | 'POST /api/users' | ...

enum を選ぶべきケース

以下のケースでは enum が適しています。

IDE のリファクタリング機能を重視する場合

typescriptenum ErrorCode {
  ValidationError = "VALIDATION_ERROR",
  AuthenticationError = "AUTHENTICATION_ERROR",
  NotFoundError = "NOT_FOUND_ERROR",
}

// ErrorCode.ValidationError を右クリック → 「シンボルの名前変更」で一括変更可能
function handleError(code: ErrorCode) {
  if (code === ErrorCode.ValidationError) {
    // ...
  }
}

実行時の値検証を頻繁に行う場合

typescriptenum OrderStatus {
  Pending = "pending",
  Processing = "processing",
  Shipped = "shipped",
  Delivered = "delivered",
}

// API レスポンスの検証
function validateOrderStatus(value: unknown): OrderStatus | null {
  if (
    typeof value === "string" &&
    Object.values(OrderStatus).includes(value as OrderStatus)
  ) {
    return value as OrderStatus;
  }
  return null;
}

データベースのスキーマと一致させたい場合

typescript// Prisma のスキーマで定義した enum と一致させる
enum UserRole {
  USER = "USER",
  ADMIN = "ADMIN",
  MODERATOR = "MODERATOR",
}

const アサーションによるハイブリッドアプローチ

検証の結果、多くのプロジェクトで最も実用的なのは、as const を使ったオブジェクト定義と union 型の組み合わせでした。

typescript// 定数オブジェクトを定義
export const USER_ROLE = {
  Admin: "admin",
  Editor: "editor",
  Viewer: "viewer",
} as const;

// union 型を導出
export type UserRole = (typeof USER_ROLE)[keyof typeof USER_ROLE];
// 'admin' | 'editor' | 'viewer'

// 値の配列も導出可能
export const USER_ROLE_VALUES = Object.values(USER_ROLE);

// 実行時検証
export function isUserRole(value: string): value is UserRole {
  return USER_ROLE_VALUES.includes(value as UserRole);
}

このパターンは enum の利点(名前空間としての定数管理、実行時検証)と union 型の利点(リテラル型推論、ライブラリ連携)を両立できます。

具体例:実務での実装パターン

実際のプロジェクトでの実装例を示します。動作確認済みのコードです。

型定義の共通化

typescript// types/status.ts
export const POST_STATUS = {
  Draft: "draft",
  Published: "published",
  Archived: "archived",
} as const;

export type PostStatus = (typeof POST_STATUS)[keyof typeof POST_STATUS];

export const POST_STATUS_VALUES = Object.values(POST_STATUS);

export function isPostStatus(value: unknown): value is PostStatus {
  return (
    typeof value === "string" &&
    POST_STATUS_VALUES.includes(value as PostStatus)
  );
}

// ラベル定義
export const POST_STATUS_LABEL: Record<PostStatus, string> = {
  draft: "下書き",
  published: "公開中",
  archived: "アーカイブ済み",
};

React コンポーネントでの活用

typescriptimport type { PostStatus } from './types/status';
import { POST_STATUS, POST_STATUS_LABEL } from './types/status';

interface StatusBadgeProps {
  status: PostStatus;
}

const STATUS_STYLES: Record<PostStatus, string> = {
  draft: 'bg-gray-200 text-gray-800',
  published: 'bg-green-200 text-green-800',
  archived: 'bg-red-200 text-red-800',
};

export function StatusBadge({ status }: StatusBadgeProps) {
  return (
    <span className={`px-2 py-1 rounded ${STATUS_STYLES[status]}`}>
      {POST_STATUS_LABEL[status]}
    </span>
  );
}

API レスポンスの検証

typescriptimport { isPostStatus, type PostStatus } from "./types/status";

interface ApiPost {
  id: number;
  title: string;
  status: PostStatus;
}

async function fetchPost(id: number): Promise<ApiPost | null> {
  const response = await fetch(`/api/posts/${id}`);
  const data = await response.json();

  // 実行時検証
  if (!isPostStatus(data.status)) {
    console.error(`Invalid status: ${data.status}`);
    return null;
  }

  return data as ApiPost;
}

状態遷移の型安全な実装

下図は、投稿ステータスの状態遷移を示しています。

mermaidstateDiagram-v2
  [*] --> draft
  draft --> published: 公開
  draft --> archived: 削除
  published --> archived: アーカイブ
  archived --> draft: 復元

投稿は下書きから公開・アーカイブへ遷移し、アーカイブから下書きへの復元も可能です。

typescriptimport type { PostStatus } from "./types/status";

const VALID_TRANSITIONS: Record<PostStatus, PostStatus[]> = {
  draft: ["published", "archived"],
  published: ["archived"],
  archived: ["draft"],
};

function canTransition(from: PostStatus, to: PostStatus): boolean {
  return VALID_TRANSITIONS[from].includes(to);
}

function transitionStatus(
  currentStatus: PostStatus,
  newStatus: PostStatus,
): PostStatus {
  if (!canTransition(currentStatus, newStatus)) {
    throw new Error(`Cannot transition from ${currentStatus} to ${newStatus}`);
  }
  return newStatus;
}

enum と union 型の判断基準まとめ

ここまでの検証結果を踏まえ、選択の判断基準を整理します。

判断フローチャート

mermaidflowchart TD
  start["定数の集合を定義したい"] --> q1{"実行時に値検証が必要?"}
  q1 -->|Yes| q2{"IDE リファクタリングを重視?"}
  q1 -->|No| union["union 型を選択"]
  q2 -->|Yes| enum["enum を選択"]
  q2 -->|No| hybrid["as const + union 型"]
  union --> done["完了"]
  enum --> done
  hybrid --> done

このフローチャートに従い、プロジェクトの要件に応じて選択してください。

詳細比較表

判断基準union 型が向くケースenum が向くケース
バンドルサイズ最小化したい気にしない
実行時検証頻度が低い頻繁に行う
ライブラリ連携Zod、React Hook Form などPrisma のスキーマと一致させたい
IDE 機能型推論の精度を重視リファクタリングを重視
チーム規模小〜中規模大規模(命名規約の統一)
TypeScript 習熟度中級以上初級〜中級

実務での推奨アプローチ

  1. デフォルトは union 型 + as const を使用する
  2. データベースの enum と一致させる必要がある場合のみ、TypeScript の enum を使用する
  3. 数値 enum は原則として避ける(型安全性の問題があるため)

まとめ

TypeScript の enum と union 型は、それぞれ異なる特性を持っています。実行時表現の有無、バンドルサイズへの影響、ライブラリとの親和性、IDE のリファクタリング機能など、プロジェクトの要件に応じて適切に選択することが重要です。

検証と実務経験を踏まえると、多くのケースでは union 型 + as const のパターンが最もバランスが良いと言えます。ただし、Prisma などデータベースの enum と一致させる必要がある場合や、大規模チームで IDE のリファクタリング機能を重視する場合は、TypeScript の enum も有効な選択肢です。

どちらか一方に統一するのではなく、用途に応じて使い分けることで、型安全性と開発効率を両立できます。

関連リンク

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;