TypeScriptのEnumとUnion型の違いで迷うエンジニア必見のベストプラクティスとコードの可読性を改善するテクニック

TypeScriptのEnumとUnion型の違いとベストプラクティスを解説する記事のアイキャッチ画像 プログラミング言語

TypeScriptで型安全性を高めたいとき、多くのエンジニアが「EnumとUnion型、どちらを使うべきか」で迷います。
特に、既存のJavaScriptコードベースからTypeScriptへ移行する場面や、チーム内でコード規約を整備するタイミングでは、この選択がその後の保守性や可読性に大きな影響を与えます。

Enumは、関連する定数をひとつの名前空間にまとめ、値の範囲を明示的に定義できる強力な機能です。
一方、Union型は"A" | "B" | "C"のようにリテラル型を組み合わせることで、より軽量かつ柔軟に「取りうる値の集合」を表現できます。
両者は一見似ていますが、コンパイル後のJavaScriptコードの挙動、IDEの補完やエラーメッセージの出方、そして「その型が何を意味しているか」という設計意図の伝わり方に、決定的な違いがあります。

  • Enumはランタイムに値が存在し、逆引き(Enum[key])も可能なため、バックエンドから受け取った数値コードをEnumにマッピングする用途に適しています
  • Union型はコンパイル後には単なるリテラル値として扱われ、バンドルサイズやパフォーマンスへの影響が少ない一方、値の範囲が広がりすぎると型定義が煩雑になりがちです
  • プロジェクトの規模やチームの習熟度、フロントエンドとバックエンドのやり取りの仕様によって、EnumとUnion型のどちらが「より自然な抽象化」になるかは変わってきます

この記事では、EnumとUnion型の言語仕様上の違い実務上の使い分けに焦点を当て、次のような観点からベストプラクティスを整理します。

  • 型安全性とランタイム挙動のトレードオフ
  • コードの可読性とIDEサポートの観点
  • 既存コードベースやAPI仕様との整合性
  • チーム開発における規約設計とレビュー観点

最後に、具体的なコード例を通じて「Enumを使うべき場面」「Union型の方が適している場面」「両者を組み合わせる設計パターン」を紹介し、読者の皆さんが迷わずに型設計を進められるように、実践的な指針を示します。

TypeScriptのEnumとUnion型の違いを整理する

TypeScriptのEnumとUnion型の違いを図解した比較表

TypeScriptで型安全なコードを書くとき、多くのエンジニアが「EnumとUnion型、どちらを使うべきか」で迷います。
両者は一見似ていますが、言語仕様レベルでの振る舞いと、実務での使いどころが大きく異なります。
このセクションでは、EnumとUnion型の基本動作型安全性の違いを整理し、その後の設計判断に役立つ基礎知識を固めます。

Enumの基本とTypeScriptにおける挙動

Enum(列挙型)は、関連する定数をひとつの名前空間にまとめて管理するためのTypeScriptの機能です。
たとえば、HTTPステータスコードを表現する場合、次のように定義できます。

enum HttpStatus {
  OK = 200,
  BadRequest = 400,
  NotFound = 404,
  InternalServerError = 500,
}

Enumの大きな特徴は、ランタイムに値が存在する点です。
TypeScriptはEnumをコンパイルすると、対応するJavaScriptオブジェクトを生成します。
そのため、HttpStatus.OK というアクセスだけでなく、HttpStatus[200] のように逆引きも可能です。
これは、バックエンドから数値コードを受け取ったときに、Enumにマッピングして意味のある名前として扱いたい場面で非常に便利です。

一方で、Enumはコンパイル後のコード量が増える傾向があり、バンドルサイズやパフォーマンスに影響を与える可能性があります。
また、数値Enumでは意図しない数値が代入されてしまうリスクもあり、型安全性の観点からは注意が必要です。

Union型の基本とリテラル型の組み合わせ

Union型は、複数の型のいずれかを受け入れる型です。
Enumと比較されるのは、リテラル型を組み合わせたUnion型です。
たとえば、HTTPステータスを文字列で表現する場合、次のように書けます。

type HttpStatus =
  | "OK"
  | "BadRequest"
  | "NotFound"
  | "InternalServerError";

Union型の特徴は、コンパイル後には単なるリテラル値として扱われる点です。
ランタイムに特別なオブジェクトは生成されないため、バンドルサイズへの影響は最小限です。
また、IDEの補完がリテラル値そのものを提示するため、コードを書く際の手触りが良いという利点もあります。

一方で、Union型は「値の集合」をそのまま型として表現するため、取りうる値の種類が増えると型定義が煩雑になりがちです。
また、逆引きのようなランタイムのマッピングは、自分でMapや関数を用意する必要があります。

EnumとUnion型の型安全性の違い

EnumとUnion型は、どちらも「取りうる値の範囲」を制限する点では共通していますが、型安全性の性質が異なります。

  • Enumは、名前空間に紐づいた値として扱われます。数値Enumの場合、コンパイラは「そのEnum型のメンバー」かどうかをチェックしますが、数値そのものの範囲までは厳密に制限しません。そのため、HttpStatus型の変数に999のような数値を代入しても、コンパイルエラーにならないことがあります
  • Union型は、リテラル値そのものを型として扱います。"OK" | "BadRequest" | ... という型は、文字通りそのリテラル値以外を受け付けません。そのため、Union型の方が「取りうる値の集合」をより厳密に表現できます

ただし、Enumには「逆引き」や「名前空間によるグルーピング」という利点があり、Union型には「軽量さ」と「厳密なリテラル制約」という利点があります。
どちらが優れているかではなく、プロジェクトの要件や設計思想に合わせて、適切な方を選ぶことが重要です。

次のセクションでは、Enumを使うべき場面とUnion型が向いているケースを、具体的なユースケースとともに整理していきます。

Enumを使うべき場面とメリット

TypeScriptのEnumが適しているユースケースと利点の図解

TypeScriptのEnumは、単に「定数をまとめる」以上の価値を持っています。
特に、ランタイムに値が存在することと、IDEとの相性の良さは、Union型では得にくい大きなメリットです。
このセクションでは、Enumが真価を発揮する場面と、その具体的な利点を整理します。

ランタイムに値が存在する利点

Enumの最大の特徴は、コンパイル後もJavaScriptオブジェクトとしてランタイムに残る点です。
たとえば、次のようなEnumを定義したとします。

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

このEnumはコンパイル後もオブジェクトとして存在するため、次のような操作が可能です。

  • バックエンドから受け取った文字列をEnumにマッピングする
  • Enumのキーをループで回してUIの選択肢を生成する
  • ログ出力やエラーメッセージで、Enumの名前を人間が読める形で表示する

Union型はコンパイル後にはリテラル値に展開されるため、「キーと値の対応関係」をランタイムで扱うことはできません。
そのため、APIレスポンスのコードをEnumに変換したい場合や、設定値のバリデーションで「有効な値の一覧」を動的に扱いたい場合には、Enumの方が適しています。

また、Enumは「名前空間」としての役割も果たします。
たとえば、UserRole.AdminProductStatus.Active は名前が同じでも別のEnumに属するため、名前の衝突を避けつつ、意味の近い定数をグループ化できます。
これは、大規模なプロジェクトで定数管理を整理する際に非常に有用です。

IDE補完とエラーメッセージの読みやすさ

EnumはIDEとの相性が非常に良いという利点もあります。
TypeScript対応のエディタでは、Enumのメンバーが補完候補として表示されるため、タイポを防ぎつつ、利用可能な値をすぐに確認できます。

たとえば、次のような関数を考えます。

function canEditContent(role: UserRole): boolean {
  return role === UserRole.Admin || role === UserRole.Editor;
}

このとき、UserRole. まで入力すると、IDEは Admin, Editor, Viewer を候補として提示します。
これにより、開発者は「どの値が有効か」をドキュメントを見なくても把握できます。

一方、Union型でも補完は効きますが、"admin" | "editor" | "viewer" のようなリテラルUnionは、値が増えると候補が多くなり、視認性が悪くなることがあります。
また、エラーメッセージも「型 ‘”foo”‘ は型 ‘UserRole’ に代入できません」のように、Enum名が明示されるため、どのEnumのどの値が期待されているのかが分かりやすくなります。

さらに、Enumは自己文書化の側面も持ちます。
UserRole.Admin という書き方は、「ユーザーの役割としての管理者」であることを明示します。
一方、単なる文字列 "admin" は、文脈によっては「設定キー」や「APIエンドポイント」など、別の意味にも取れてしまう可能性があります。

まとめると、Enumは次のような場面で特に有効です。

  • バックエンドとのやり取りでコード値や文字列をEnumにマッピングしたい
  • 有効な値の一覧をランタイムで扱いたい(選択肢生成やバリデーション)
  • 名前空間として定数をグループ化し、名前の衝突を避けたい
  • IDE補完とエラーメッセージの読みやすさを重視したい

次のセクションでは、Union型が向いているケースと、Enumとの使い分けの指針について詳しく見ていきます。

Union型が向いているケースと設計上の利点

Union型が適している設計パターンとコード例

Enumが「ランタイムに値が存在する」という強みを持つ一方で、Union型は軽量さ柔軟性を武器に、多くの場面でEnumよりも自然な選択肢になります。
特に、フロントエンドのバンドルサイズやパフォーマンスを気にする場面、そしてAPIレスポンスのマッピングのように「値そのもの」を重視する設計では、Union型の方が適していることが多いです。
このセクションでは、Union型が向いている具体的なケースと、その設計上の利点を整理します。

バンドルサイズとパフォーマンスへの影響

Union型の最大の利点のひとつは、コンパイル後には単なるリテラル値に展開される点です。
たとえば、次のようなUnion型を定義したとします。

type Theme = "light" | "dark" | "auto";

この型はコンパイル後、"light""dark" といった文字列リテラルとして扱われます。
ランタイムに特別なオブジェクトは生成されないため、バンドルサイズへの影響は最小限です。

一方、Enumを同じ目的で使うと、次のようになります。

enum Theme {
  Light = "light",
  Dark = "dark",
  Auto = "auto",
}

このEnumはコンパイル後、Theme というオブジェクトを生成します。
プロジェクトが大規模になり、Enumの数が増えると、このオブジェクトの分だけバンドルサイズが増加します。
特に、SPAやモバイルアプリのように、初回ロードのパフォーマンスが重要な場面では、Union型の軽量さは無視できないメリットです。

また、Tree Shakingの観点でも、Union型は有利です。
Enumはオブジェクトとして存在するため、使用していないメンバーがあっても完全には削除されないことがあります。
一方、Union型はリテラル値として扱われるため、未使用の値はバンドルから自然に消えやすくなります。

文字列リテラル型とAPIレスポンスのマッピング

Union型は、APIレスポンスのマッピングにおいても非常に強力です。
多くのREST APIは、ステータスや種別を文字列で返します。
たとえば、ユーザーのステータスが "active" | "inactive" | "suspended" のいずれかで返ってくるとします。

このとき、Union型を使えば、受け取った値をそのまま型安全に扱えます。

type UserStatus = "active" | "inactive" | "suspended";

interface User {
  id: string;
  status: UserStatus;
}

APIから受け取った status は、すでに UserStatus 型として扱えるため、追加の変換ロジックは不要です。
Enumを使う場合、受け取った文字列をEnumにマッピングする処理が必要になりますが、Union型であればそのまま型として扱えるため、コードがシンプルになります。

また、Union型は拡張性にも優れています。
たとえば、新しいステータス "pending" が追加された場合、Union型の定義を更新するだけで、既存のコードで "pending" が未対応であることが型エラーとして検出されます。

一方、Enumで同じことをする場合、Enumの定義を更新し、マッピング関数も修正する必要があります。
Union型は「値そのもの」を型として扱うため、API仕様の変更に素早く追従しやすいという利点があります。

まとめると、Union型は次のような場面で特に有効です。

  • バンドルサイズやパフォーマンスを重視するフロントエンドアプリケーション
  • APIレスポンスの値そのものを型として扱いたい場合
  • 値の種類が頻繁に変わる可能性がある設計
  • 軽量でシンプルな型定義を求めるとき

次のセクションでは、EnumとUnion型の混在を避けるための設計パターンと、既存コードベースとの整合性の取り方について詳しく見ていきます。

EnumとUnion型の混在を避ける設計パターン

EnumとUnion型の混在を避けるための設計指針

TypeScriptのプロジェクトが成長するにつれて、EnumとUnion型が混在し、「同じ概念なのに定義がバラバラ」という状態になることがあります。
たとえば、ユーザーの役割を表す型が、あるファイルではEnum、別のファイルではUnion型で定義されていると、コードの一貫性が損なわれ、バグの温床になります。
このセクションでは、EnumとUnion型の混在を避けるための設計パターンと、既存コードベースやチーム開発での運用方法を整理します。

既存コードベースとの整合性をどう取るか

既存のJavaScriptコードベースをTypeScriptに移行する場合、すでに「定数オブジェクト」や「文字列リテラルの集合」が定義されていることが多いです。
ここで安易にEnumに置き換えると、既存のロジックやテストが壊れるリスクがあります。
一方で、Union型に寄せすぎると、ランタイムでのマッピングが煩雑になる可能性もあります。

既存コードベースとの整合性を取るには、次のようなステップが有効です。

  • まず、既存の定数定義を洗い出し、どのような用途で使われているかを整理します。APIレスポンスのマッピングに使われているのか、UIの選択肢生成に使われているのか、ログ出力に使われているのか、といった観点です
  • 次に、用途ごとに「Enumが向いているか」「Union型が向いているか」を判断します。たとえば、APIレスポンスのコード値が数値で、逆引きが必要な場合はEnumが適しています。一方、UIのテーマ名のように文字列リテラルで十分な場合はUnion型が適しています
  • 最後に、一貫性のある方針を決めます。たとえば、「バックエンド連携はEnum、フロントエンド内部の状態はUnion型」といったルールを設けることで、混在を最小限に抑えられます

また、移行フェーズでは、既存の定数オブジェクトをUnion型やEnumに段階的に置き換えることも重要です。
いきなりすべてを書き換えるのではなく、新しいコードからルールに沿った型を使い始め、既存コードはリファクタリングのタイミングで少しずつ修正していく、というアプローチが現実的です。

チーム開発での規約設計とレビュー観点

チーム開発では、個人の好みでEnumとUnion型が混在すると、コードベースの一貫性が損なわれます。
そのため、規約として使い分けルールを明文化することが重要です。

具体的には、次のような観点でルールを設計します。

  • 用途ベースのルール: 「ステータスコードはEnum、UIの状態はUnion型」のように、用途ごとに推奨する型を決めます
  • 命名規則: Enumの名前は単数形かつPascalCase、Union型の名前はPascalCaseだが値はcamelCaseやkebab-case、といった命名規則を統一します
  • ドキュメント: なぜその型を選んだのかをコメントやドキュメントに残し、後から見たときに判断理由が分かるようにします

レビュー時には、次のような観点でチェックします。

  • 同じ概念がEnumとUnion型の両方で定義されていないか
  • 新しい定数が追加されたとき、既存の型定義と一貫性があるか
  • パフォーマンスやバンドルサイズの観点から、Union型に寄せるべき箇所でEnumを使いすぎていないか

また、ユーティリティ型やヘルパー関数を用意することで、混在を防ぎやすくなります。
たとえば、EnumからUnion型を生成するユーティリティ型を用意しておけば、Enumを「単一の情報源」として扱い、Union型はそこから導出する、という設計が可能です。

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

type UserRoleUnion = `${UserRole}`; // "admin" | "editor" | "viewer"

このように、Enumを「源泉」とし、Union型を「利用側の型」として扱うことで、混在を避けつつ、両者の利点を活かすことができます。

まとめると、EnumとUnion型の混在を避けるには、次のような方針が有効です。

  • 既存コードベースの用途を整理し、用途ごとにEnumかUnion型かを決める
  • チームで一貫性のあるルールを設け、レビューでチェックする
  • Enumを源泉とし、Union型を導出する設計パターンを活用する

次のセクションでは、コードの可読性を高める具体的なテクニックとして、命名規則やドキュメントコメントの活用方法について詳しく見ていきます。

コードの可読性を高める具体的なテクニック

EnumとUnion型の可読性を高める命名と構造化の例

EnumとUnion型の使い分けが決まっても、実際のコードが読みにくければ、型安全性のメリットは半減してしまいます。
特に、チーム開発では「他人が書いたコードをすぐに理解できるか」が重要です。
このセクションでは、命名規則とドキュメントコメント、そしてユーティリティ型によるUnion型の安全性向上という2つの観点から、コードの可読性を高める具体的なテクニックを紹介します。

命名規則とドキュメントコメントの活用

EnumとUnion型は、どちらも「取りうる値の集合」を表現しますが、その意味をコードから読み取れるようにすることが重要です。
そのために、命名規則とドキュメントコメントを活用します。

まず、命名規則です。
EnumとUnion型では、役割が少し異なるため、次のような方針が有効です。

  • Enumは「概念の集合」を表すため、単数形かつPascalCaseで命名します。例: UserRole, HttpStatus, ProductCategory
  • Union型は「値の集合」を表すため、PascalCaseの型名と、camelCaseやkebab-caseのリテラル値を組み合わせます。例: type Theme = "light" | "dark" | "auto";

次に、ドキュメントコメントです。
JSDocやTSDocを使い、次のような情報を明示します。

  • この型が何を表しているか(ビジネス上の意味)
  • どのような値が含まれるか(例: 「ユーザーの役割を表す。管理者、編集者、閲覧者のいずれか」)
  • 将来の変更可能性(例: 「新しい役割が追加される可能性がある」)

たとえば、次のように書くことで、後から見たときに意図が明確になります。

/**
 * ユーザーの役割を表す列挙型。
 * - Admin: システム全体の管理権限を持つ
 * - Editor: コンテンツの編集権限を持つ
 * - Viewer: 閲覧のみ可能
 */
enum UserRole {
  Admin = "admin",
  Editor = "editor",
  Viewer = "viewer",
}

/**
 * アプリケーションのテーマを表すUnion型。
 * - "light": 明るいテーマ
 * - "dark": 暗いテーマ
 * - "auto": OSの設定に追随
 */
type Theme = "light" | "dark" | "auto";

このように、型名だけでなく、値の意味もコメントで補足することで、コードを読む人が「なぜこの値があるのか」を理解しやすくなります。

ユーティリティ型でUnion型をさらに安全に

Union型は軽量で柔軟ですが、値が増えると型定義が煩雑になりがちです。
また、「この値はこの関数では使えない」といったより細かい制約を表現したい場合もあります。
そこで、TypeScriptのユーティリティ型を活用して、Union型をさらに安全に、かつ読みやすくします。

たとえば、すべてのユーザー役割を受け入れる関数と、管理者のみを受け入れる関数を考えます。

type UserRole = "admin" | "editor" | "viewer";

// すべての役割を受け入れる
function canAccessDashboard(role: UserRole): boolean {
  return role !== "viewer";
}

// 管理者のみを受け入れる
function canDeleteUser(role: Extract<UserRole, "admin">): boolean {
  return true;
}

ここで、Extract<UserRole, "admin"> は、UserRole から "admin" のみを抽出した型です。
これにより、canDeleteUser には "admin" 以外を渡そうとするとコンパイルエラーになります。
このように、Union型の一部だけを受け入れる関数を型安全に表現できます。

また、ExcludePick, Omit などのユーティリティ型を組み合わせることで、次のようなことが可能です。

  • Exclude<UserRole, "viewer"> で「閲覧者以外」の役割を表現する
  • 特定のプロパティがUnion型を持つオブジェクトから、そのUnion型だけを抽出する
  • 既存のUnion型から、特定の値を除外した新しいUnion型を定義する

さらに、satisfies 演算子を使うことで、Union型の値が期待通りに使われているかをチェックできます。

const roles = {
  admin: "admin",
  editor: "editor",
  viewer: "viewer",
} satisfies Record<string, UserRole>;

このように、ユーティリティ型を活用することで、Union型の意図をより明確に表現し、誤用を防ぐことができます。
結果として、コードの可読性と型安全性の両方が向上します。

まとめると、コードの可読性を高めるには、次のようなテクニックが有効です。

  • EnumとUnion型の命名規則を統一し、意味が伝わる名前を付ける
  • ドキュメントコメントで型の意味と値の役割を補足する
  • ユーティリティ型でUnion型の制約を明確にし、誤用を防ぐ

次のセクションでは、実例を通じてEnumとUnion型の使い分けを具体的に見ていきます。

実例で見るEnumとUnion型の使い分け

実例コードで見るEnumとUnion型の使い分けの比較

理論的な違いを理解したところで、実際の開発現場で「EnumとUnion型のどちらを使うべきか」を判断するのは、まだ難しいかもしれません。
このセクションでは、ステータスコードやエラー種別の表現UIの状態管理やコンポーネントのprops設計という2つの具体的なシナリオを通じて、EnumとUnion型の使い分けを実践的に解説します。

ステータスコードやエラー種別の表現

HTTPステータスコードやアプリケーション内のエラー種別は、多くの場合「数値コード」や「文字列コード」で表現されます。
ここでEnumとUnion型のどちらを使うかは、バックエンドとの連携方法ランタイムでの扱い方によって決まります。

たとえば、REST APIからHTTPステータスコードが数値で返ってくる場合を考えます。
フロントエンドでは、このコードを人間が読める形に変換したり、ログに出力したりしたいことがあります。
このような場面では、Enumが適しています。

enum HttpStatus {
  OK = 200,
  BadRequest = 400,
  Unauthorized = 401,
  NotFound = 404,
  InternalServerError = 500,
}

function handleResponse(status: number): string {
  if (status === HttpStatus.OK) {
    return "成功";
  }
  // 逆引きも可能
  const statusName = HttpStatus[status];
  return `エラー: ${statusName} (${status})`;
}

Enumを使うことで、200 という数値が HttpStatus.OK という意味を持つことが明確になります。
また、HttpStatus[200] のように逆引きできるため、ログ出力やエラーメッセージの生成が簡単です。

一方、アプリケーション内のエラー種別が文字列で定義されている場合、Union型が適していることが多いです。

type AppError =
  | "NETWORK_ERROR"
  | "VALIDATION_ERROR"
  | "AUTH_ERROR"
  | "UNKNOWN_ERROR";

function showErrorToast(error: AppError): void {
  // errorは文字列リテラルとして扱える
  console.log(`エラー: ${error}`);
}

この場合、エラー種別はAPIレスポンスのマッピングや逆引きよりも、UIでの表示や条件分岐が主な用途です。
Union型であれば、バンドルサイズを抑えつつ、型安全にエラー種別を扱えます。

UIの状態管理やコンポーネントのprops設計

フロントエンドのUIでは、ボタンの状態やモーダルの開閉状態など、多くの「状態」を管理します。
これらの状態は、多くの場合文字列リテラルで十分であり、ランタイムでの逆引きやマッピングは不要です。
そのため、Union型が適していることが多いです。

たとえば、ボタンの状態を表す型を考えます。

type ButtonState = "idle" | "loading" | "success" | "error";

interface ButtonProps {
  state: ButtonState;
  onClick: () => void;
}

このようにUnion型で定義すると、コンポーネントのpropsがシンプルになり、利用側も「どの状態が有効か」を一目で把握できます。
また、新しい状態を追加する場合も、Union型の定義を更新するだけで、未対応の箇所が型エラーとして検出されます。

一方、UIのテーマや言語設定のように、設定値として保存・復元したい場合には、Enumが適していることがあります。

enum Theme {
  Light = "light",
  Dark = "dark",
  Auto = "auto",
}

function saveTheme(theme: Theme): void {
  localStorage.setItem("theme", theme);
}

function loadTheme(): Theme {
  const saved = localStorage.getItem("theme");
  // バリデーションが必要だが、Enumの値一覧を利用できる
  if (Object.values(Theme).includes(saved as Theme)) {
    return saved as Theme;
  }
  return Theme.Auto;
}

ここでは、Theme の値一覧を Object.values(Theme) で取得できるため、ローカルストレージからの復元時のバリデーションが簡単になります。
Union型でも同様のことはできますが、値の一覧を別途定義する必要があります。

まとめると、次のような指針が有効です。

  • ステータスコードやエラー種別のように、数値コードや文字列コードを意味のある名前にマッピングしたい場合や、逆引きが必要な場合はEnumが向いています
  • UIの状態やpropsのように、値そのものを条件分岐や表示に使うことが主な用途で、ランタイムでのマッピングが不要な場合はUnion型が向いています
  • 設定値のように、値の一覧をランタイムで扱いたい場合には、Enumの利点が活きることがあります

次のセクションでは、これまでの内容を踏まえ、EnumとUnion型のベストプラクティスを総まとめします。

EnumとUnion型のベストプラクティス総まとめ

TypeScriptのEnumとUnion型のベストプラクティスをまとめた図解

ここまで、TypeScriptのEnumとUnion型の違い、それぞれが向いている場面、混在を避ける設計パターン、そしてコードの可読性を高めるテクニックについて詳しく見てきました。
このセクションでは、それらを踏まえ、EnumとUnion型のベストプラクティスを総まとめします。
実務で迷わずに型設計を進めるための、実践的な指針としてお役立ていただければと思います。

まず、EnumとUnion型の根本的な違いを再確認しておきます。
Enumは「ランタイムに値が存在する列挙型」であり、Union型は「コンパイル後にはリテラル値に展開される型の和集合」です。
この違いから、次のような特性が生まれます。

  • Enumは、逆引きや値の一覧取得が可能で、バックエンドとの連携や設定値の管理に適しています
  • Union型は、バンドルサイズが軽く、APIレスポンスのマッピングやUIの状態管理に適しています

この特性を踏まえ、用途ベースでの使い分けが最も重要です。
次のような指針が有効です。

  • Enumを使うべき場面
  • バックエンドから受け取った数値コードや文字列コードを、意味のある名前として扱いたい
  • 値の一覧をランタイムで扱いたい(選択肢生成、バリデーション、ログ出力など)
  • 名前空間として定数をグループ化し、名前の衝突を避けたい
  • IDE補完とエラーメッセージの読みやすさを重視したい

  • Union型を使うべき場面

  • バンドルサイズやパフォーマンスを重視するフロントエンドアプリケーション
  • APIレスポンスの値そのものを型として扱いたい(追加のマッピングを避けたい)
  • UIの状態やコンポーネントのpropsなど、値そのものを条件分岐や表示に使うことが主な用途
  • 値の種類が頻繁に変わる可能性がある設計

次に、混在を避ける設計パターンです。
EnumとUnion型が同じ概念に対してバラバラに定義されていると、コードの一貫性が損なわれます。
これを防ぐために、次のような方針が有効です。

  • 既存コードベースの用途を整理し、用途ごとにEnumかUnion型かを決める
  • チームで一貫性のあるルールを設け、レビューでチェックする
  • Enumを「源泉」とし、Union型を「利用側の型」として扱う設計パターンを活用する

たとえば、EnumからUnion型を導出するユーティリティ型を使うことで、混在を避けつつ両者の利点を活かせます。

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

type UserRoleUnion = `${UserRole}`; // "admin" | "editor" | "viewer"

コードの可読性を高めるテクニックとして、命名規則とドキュメントコメント、そしてユーティリティ型の活用が重要です。

  • Enumは単数形かつPascalCase、Union型はPascalCaseの型名とcamelCaseやkebab-caseのリテラル値を組み合わせる
  • JSDocやTSDocで型の意味と値の役割を補足する
  • ExtractExclude などのユーティリティ型で、Union型の制約を明確にする

最後に、実例での使い分けを振り返ります。

  • ステータスコードやエラー種別のように、数値コードや文字列コードを意味のある名前にマッピングしたい場合や逆引きが必要な場合はEnumが向いています
  • UIの状態やpropsのように、値そのものを条件分岐や表示に使うことが主な用途で、ランタイムでのマッピングが不要な場合はUnion型が向いています
  • 設定値のように、値の一覧をランタイムで扱いたい場合には、Enumの利点が活きることがあります

まとめると、EnumとUnion型のベストプラクティスは、「用途に応じて適切な方を選び、一貫性を持って使い、可読性を高める工夫をする」という一言に集約できます。
この指針を意識することで、TypeScriptの型システムを最大限に活用し、保守性の高いコードベースを構築できるはずです。

EnumとUnion型の違いで迷わないための指針

TypeScriptのEnumとUnion型の違いと使い分け指針のまとめ

TypeScriptのEnumとUnion型は、どちらも「取りうる値の範囲」を制限する点では共通していますが、その設計思想と実装上の挙動は大きく異なります。
この違いを理解せずに「とりあえずEnum」や「とりあえずUnion型」で書いてしまうと、後からコードの保守性やパフォーマンスに悪影響が出る可能性があります。
このセクションでは、EnumとUnion型の違いで迷わないための実践的な指針を、これまでの内容を踏まえて整理します。

まず、EnumとUnion型の根本的な違いを一言で表すと、次のようになります。

  • Enumは「ランタイムに値が存在する列挙型」であり、コンパイル後もJavaScriptオブジェクトとして残ります
  • Union型は「コンパイル後にはリテラル値に展開される型の和集合」であり、ランタイムに特別なオブジェクトは生成されません

この違いから、次のような特性が生まれます。

  • Enumは、逆引きや値の一覧取得が可能で、バックエンドとの連携や設定値の管理に適しています
  • Union型は、バンドルサイズが軽く、APIレスポンスのマッピングやUIの状態管理に適しています

この特性を踏まえ、迷ったときの判断基準として、次のような質問を自分に投げかけると良いでしょう。

  • この値はランタイムで逆引きや一覧取得が必要ですか?
  • この値はバックエンドから受け取ったコード値ですか?
  • この値は設定として保存・復元したいですか?
  • この値はUIの状態やpropsとして、条件分岐や表示に使うことが主な用途ですか?
  • この値の種類は頻繁に変わる可能性がありますか?

これらの質問に「はい」が多ければEnum、「いいえ」が多ければUnion型が向いている可能性が高いです。
ただし、これはあくまで傾向であり、プロジェクトの要件やチームの設計思想によって最適解は変わります。
そのため、チームで一貫性のあるルールを設けることが重要です。

たとえば、次のようなルールを設けると、迷いが減ります。

  • バックエンド連携はEnum、フロントエンド内部の状態はUnion型
  • 設定値はEnum、UIの状態はUnion型
  • 新しい定数はまずUnion型で定義し、必要に応じてEnumに昇格させる

また、混在を避ける設計パターンも重要です。
EnumとUnion型が同じ概念に対してバラバラに定義されていると、コードの一貫性が損なわれます。
これを防ぐために、Enumを「源泉」とし、Union型を「利用側の型」として扱う設計が有効です。

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

type UserRoleUnion = `${UserRole}`; // "admin" | "editor" | "viewer"

このように、EnumからUnion型を導出することで、単一の情報源を保ちつつ、両者の利点を活かせます。

最後に、コードの可読性を高める工夫も忘れてはいけません。
命名規則やドキュメントコメント、ユーティリティ型の活用によって、EnumとUnion型の意図を明確に伝えることができます。

  • Enumは単数形かつPascalCase、Union型はPascalCaseの型名とcamelCaseやkebab-caseのリテラル値を組み合わせる
  • JSDocやTSDocで型の意味と値の役割を補足する
  • ExtractExclude などのユーティリティ型で、Union型の制約を明確にする

まとめると、EnumとUnion型の違いで迷わないための指針は、「用途に応じて適切な方を選び、一貫性を持って使い、可読性を高める工夫をする」という一言に集約できます。
この指針を意識することで、TypeScriptの型システムを最大限に活用し、保守性の高いコードベースを構築できるはずです。

コメント

タイトルとURLをコピーしました