TypeScriptのenumとunion型を比較・検証する どちらを選ぶべきか判断基準を整理
TypeScript で定数の集合を表現する際、enum と union 型のどちらを選ぶべきか迷う場面は多いです。本記事では、実行時表現の有無・拡張性・型安全性の観点から両者を比較し、実務での判断基準を整理します。静的型付けを活かした設計を検討している方、チーム開発でコーディング規約を策定したい方に向けて、検証結果と実体験をもとに解説します。
enum と union 型の比較
| 観点 | enum | union 型 |
|---|---|---|
| 実行時表現 | 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 習熟度 | 中級以上 | 初級〜中級 |
実務での推奨アプローチ
- デフォルトは union 型 +
as constを使用する - データベースの enum と一致させる必要がある場合のみ、TypeScript の enum を使用する
- 数値 enum は原則として避ける(型安全性の問題があるため)
まとめ
TypeScript の enum と union 型は、それぞれ異なる特性を持っています。実行時表現の有無、バンドルサイズへの影響、ライブラリとの親和性、IDE のリファクタリング機能など、プロジェクトの要件に応じて適切に選択することが重要です。
検証と実務経験を踏まえると、多くのケースでは union 型 + as const のパターンが最もバランスが良いと言えます。ただし、Prisma などデータベースの enum と一致させる必要がある場合や、大規模チームで IDE のリファクタリング機能を重視する場合は、TypeScript の enum も有効な選択肢です。
どちらか一方に統一するのではなく、用途に応じて使い分けることで、型安全性と開発効率を両立できます。
関連リンク
著書
article2026年1月23日TypeScriptのtypeとinterfaceを比較・検証する 違いと使い分けの判断基準を整理
article2026年1月23日TypeScript 5.8の型推論を比較・検証する 強化点と落とし穴の回避策
article2026年1月23日TypeScript Genericsの使用例を早見表でまとめる 記法と頻出パターンを整理
article2026年1月22日TypeScriptの型システムを概要で理解する 基礎から全体像まで完全解説
article2026年1月22日ZustandとTypeScriptのユースケース ストアを型安全に設計して運用する実践
article2026年1月22日TypeScriptでよく出るエラーをトラブルシュートでまとめる 原因と解決法30選
articleshadcn/ui × TanStack Table 設計術:仮想化・列リサイズ・アクセシブルなグリッド
articleRemix のデータ境界設計:Loader・Action とクライアントコードの責務分離
articlePreact コンポーネント設計 7 原則:再レンダリング最小化の分割と型付け
articlePHP 8.3 の新機能まとめ:readonly クラス・型強化・性能改善を一気に理解
articleNotebookLM に PDF/Google ドキュメント/URL を取り込む手順と最適化
articlePlaywright 並列実行設計:shard/grep/fixtures で高速化するテストスイート設計術
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
