TypeScript enumを実務で使いこなす:実装パターンと注意点まで徹底解説

TypeScript

TypeScript enumを実務で使いこなす:実装パターンと注意点まで徹底解説

TypeScript enumとは

TypeScript enumは、関連する定数値の集合に名前をつけるための言語機能です。実務では単なる定数管理ツールではなく、型安全性とコードの可読性を同時に向上させる重要な存在です。

enumを使うことで、以下のメリットが得られます:

  • ハードコードされた値を減らせる
  • 開発中の入力補完が効く
  • コンパイル時の型チェックで実行時エラーを防ぐ
  • チームメンバーが取り得る値を一目瞭然に理解できる

実務でよく使われるユースケース

enumが活躍するシーンを3つご紹介します。

1. ステータス管理

最も一般的な使い方は、オーダー処理やユーザー状態などの状態管理です。複数の状態を体系的に管理する場合に重宝します。

2. APIレスポンスの型定義

バックエンドから返ってくる値が限定されている場合、enumで定義することで不正な値の混入を防げます。

3. 設定値やオプションの管理

ユーザーが選択可能なオプション(ログレベル、ソート順序など)をenumで管理することで、実装ミスを減らせます。

実務的な実装コード

ケース1:注文状態の管理

ECサイトの注文管理システムを想定したコード例です。

// ❌ こういう書き方は避けましょう
const orderStatus = {
  pending: 'pending',
  confirmed: 'confirmed',
  shipped: 'shipped',
  delivered: 'delivered',
  cancelled: 'cancelled'
};

// ✅ こういう書き方をしましょう
enum OrderStatus {
  Pending = 'pending',
  Confirmed = 'confirmed',
  Shipped = 'shipped',
  Delivered = 'delivered',
  Cancelled = 'cancelled'
}

// 型定義で使用
interface Order {
  id: string;
  customerId: string;
  status: OrderStatus;
  createdAt: Date;
  updatedAt: Date;
  amount: number;
}

// ステータス遷移の管理
class OrderService {
  // 次に遷移可能な状態をenumで定義
  private readonly statusTransitions: Record = {
    [OrderStatus.Pending]: [OrderStatus.Confirmed, OrderStatus.Cancelled],
    [OrderStatus.Confirmed]: [OrderStatus.Shipped, OrderStatus.Cancelled],
    [OrderStatus.Shipped]: [OrderStatus.Delivered],
    [OrderStatus.Delivered]: [],
    [OrderStatus.Cancelled]: []
  };

  // 状態遷移の妥当性チェック
  canTransitionTo(currentStatus: OrderStatus, nextStatus: OrderStatus): boolean {
    return this.statusTransitions[currentStatus].includes(nextStatus);
  }

  // 注文のステータスを更新
  updateOrderStatus(order: Order, newStatus: OrderStatus): Order | null {
    if (!this.canTransitionTo(order.status, newStatus)) {
      console.error(
        `Cannot transition from ${order.status} to ${newStatus}`
      );
      return null;
    }

    return {
      ...order,
      status: newStatus,
      updatedAt: new Date()
    };
  }
}

// 使用例
const service = new OrderService();
const order: Order = {
  id: '12345',
  customerId: 'cust001',
  status: OrderStatus.Pending,
  createdAt: new Date(),
  updatedAt: new Date(),
  amount: 10000
};

// ✅ 型チェックが効く
const updated = service.updateOrderStatus(order, OrderStatus.Confirmed);

// ❌ これはコンパイルエラーになる
// const invalid = service.updateOrderStatus(order, 'invalid_status');

ケース2:APIレスポンスの型安全性

外部APIとの連携では、レスポンスの値が想定外になることがあります。enumを使ってそれを防ぎます。

// APIからのレスポンス型
enum PaymentMethod {
  CreditCard = 'credit_card',
  BankTransfer = 'bank_transfer',
  ConvenienceStore = 'convenience_store',
  Wallet = 'wallet'
}

enum PaymentStatus {
  Pending = 'pending',
  Completed = 'completed',
  Failed = 'failed',
  Refunded = 'refunded'
}

interface PaymentResponse {
  id: string;
  method: PaymentMethod;
  status: PaymentStatus;
  amount: number;
  timestamp: string;
}

// APIクライアント
class PaymentApiClient {
  async getPaymentInfo(paymentId: string): Promise {
    const response = await fetch(`/api/payments/${paymentId}`);
    const data = await response.json();

    // 型ガード関数で安全にenumに変換
    const method = this.parsePaymentMethod(data.method);
    const status = this.parsePaymentStatus(data.status);

    if (!method || !status) {
      throw new Error('Invalid payment data from API');
    }

    return {
      id: data.id,
      method,
      status,
      amount: data.amount,
      timestamp: data.timestamp
    };
  }

  private parsePaymentMethod(value: any): PaymentMethod | null {
    if (Object.values(PaymentMethod).includes(value)) {
      return value;
    }
    return null;
  }

  private parsePaymentStatus(value: any): PaymentStatus | null {
    if (Object.values(PaymentStatus).includes(value)) {
      return value;
    }
    return null;
  }
}

// 使用例
const client = new PaymentApiClient();
const payment = await client.getPaymentInfo('pay_12345');

// switch文で全ケースをカバーしないとエラーになる
function handlePayment(payment: PaymentResponse) {
  switch (payment.status) {
    case PaymentStatus.Pending:
      console.log('支払い待機中...');
      break;
    case PaymentStatus.Completed:
      console.log('支払い完了');
      break;
    case PaymentStatus.Failed:
      console.log('支払い失敗');
      break;
    case PaymentStatus.Refunded:
      console.log('払い戻し完了');
      break;
    // ケースを追加し忘れるとTypeScriptが警告する
  }
}

ケース3:ログレベルの管理

ロギングライブラリではログレベルをenumで管理することが多いです。

enum LogLevel {
  Debug = 0,
  Info = 1,
  Warn = 2,
  Error = 3,
  Fatal = 4
}

interface LogEntry {
  timestamp: Date;
  level: LogLevel;
  message: string;
  context?: Record;
}

class Logger {
  private currentLevel: LogLevel = LogLevel.Info;

  setLevel(level: LogLevel): void {
    this.currentLevel = level;
  }

  private log(level: LogLevel, message: string, context?: Record): void {
    // 現在のログレベル以上のログのみ出力
    if (level < this.currentLevel) {
      return;
    }

    const levelName = LogLevel[level];
    const entry: LogEntry = {
      timestamp: new Date(),
      level,
      message,
      context
    };

    console.log(`[${levelName}] ${message}`, context || '');
  }

  debug(message: string, context?: Record): void {
    this.log(LogLevel.Debug, message, context);
  }

  info(message: string, context?: Record): void {
    this.log(LogLevel.Info, message, context);
  }

  warn(message: string, context?: Record): void {
    this.log(LogLevel.Warn, message, context);
  }

  error(message: string, context?: Record): void {
    this.log(LogLevel.Error, message, context);
  }

  fatal(message: string, context?: Record): void {
    this.log(LogLevel.Fatal, message, context);
  }
}

// 使用例
const logger = new Logger();
logger.setLevel(LogLevel.Info);
logger.debug('This message will not appear');
logger.info('Application started', { version: '1.0.0' });
logger.error('Database connection failed', { code: 'ECONNREFUSED' });

実務で使える応用パターン

パターン1:enumの値を動的に取得

enumの全値が必要な場合があります。特にセレクトボックスのオプション生成時に活躍します。

enum UserRole {
  Admin = 'admin',
  Moderator = 'moderator',
  User = 'user',
  Guest = 'guest'
}

// enumの全値を配列として取得
function getAllRoles(): UserRole[] {
  return Object.values(UserRole);
}

// enumの全値をラベル付きで取得
const roleLabels: Record = {
  [UserRole.Admin]: '管理者',
  [UserRole.Moderator]: 'モデレーター',
  [UserRole.User]: 'ユーザー',
  [UserRole.Guest]: 'ゲスト'
};

function getRoleOptions() {
  return getAllRoles().map(role => ({
    value: role,
    label: roleLabels[role]
  }));
}

// React/Vue等のフロントエンドで利用
// 

パターン2:複数の関連データを保持するenum

enumは値だけでなく、複数のプロパティを持つオブジェクトと組み合わせると実務でより強力になります。

enum PaymentGateway {
  Stripe = 'stripe',
  PayPal = 'paypal',
  Braintree = 'braintree'
}

// enumと紐づくメタデータ
const paymentGatewayConfig: Record = {
  [PaymentGateway.Stripe]: {
    name: 'Stripe',
    fee: 2.9,
    maxAmount: 999999,
    supportedCountries: ['US', 'JP', 'GB', 'CA']
  },
  [PaymentGateway.PayPal]: {
    name: 'PayPal',
    fee: 3.49,
    maxAmount: 100000,
    supportedCountries: ['US', 'JP']
  },
  [PaymentGateway.Braintree]: {
    name: 'Braintree',
    fee: 2.99,
    maxAmount: 500000,
    supportedCountries: ['US', 'CA', 'AU']
  }
};

// 手数料を計算する関数
function calculateFee(gateway: PaymentGateway, amount: number): number {
  const config = paymentGatewayConfig[gateway];
  return amount * (config.fee / 100);
}

// 利用可能かチェックする関数
function isGatewayAvailable(gateway: PaymentGateway, country: string, amount: number): boolean {
  const config = paymentGatewayConfig[gateway];
  return (
    config.supportedCountries.includes(country) &&
    amount <= config.maxAmount
  );
}

// 使用例
const gateway = PaymentGateway.Stripe;
const amount = 15000;
const fee = calculateFee(gateway, amount);
const available = isGatewayAvailable(gateway, 'JP', amount);

パターン3:型安全なenumフィルタリング

大規模なシステムではenumの一部のみを使いたいケースがあります。

enum UserPermission {
  CreateUser = 'create_user',
  DeleteUser = 'delete_user',
  EditUser = 'edit_user',
  ViewAnalytics = 'view_analytics',
  EditSettings = 'edit_settings',
  ViewLogs = 'view_logs'
}

// ロールごとに許可するパーミッション
const rolePermissions: Record = {
  [UserRole.Admin]: [
    UserPermission.CreateUser,
    UserPermission.DeleteUser,
    UserPermission.EditUser,
    UserPermission.ViewAnalytics,
    UserPermission.EditSettings,
    UserPermission.ViewLogs
  ],
  [UserRole.Moderator]: [
    UserPermission.EditUser,
    UserPermission.ViewLogs
  ],
  [UserRole.User]: [],
  [UserRole.Guest]: []
};

// パーミッションチェック
function hasPermission(role: UserRole, permission: UserPermission): boolean {
  return rolePermissions[role].includes(permission);
}

// 使用例
if (hasPermission(UserRole.Admin, UserPermission.EditSettings)) {
  // 設定画面を表示
}

実務で気をつけるべき注意点

注意1:文字列enumと数値enumの使い分け

TypeScriptのenumは文字列と数値の両方をサポートしていますが、実務では文字列enumを推奨します。

// ❌ 数値enum(デバッグ時に値が分かりにくい)
enum Status {
  Pending,
  Active,
  Inactive
}
// コンパイル後:{ \"0\": \"Pending\", \"1\": \"Active\", \"2\": \"Inactive\" }

// ✅ 文字列enum(デバッグが容易)
enum Status {
  Pending = 'pending',
  Active = 'active',
  Inactive = 'inactive'
}

// コンパイル後:{ \"Pending\": \"pending\", \"Active\": \"active\", \"Inactive\": \"inactive\" }

数値enumはバイナリサイズが小さいメリットがありますが、デバッグやAPI通信時の可読性を考えると文字列enumの方が実務的です。

注意2:as constを使う選択肢

最近はenumの代わりに、as constを使う流れもあります。どちらを選ぶかは現在のプロジェクト方針に合わせることが大切です。

// enumを使う方法
enum Color {
  Red = 'red',
  Blue = 'blue',
  Green = 'green'
}

// as constを使う方法
const Color = {
  Red: 'red',
  Blue: 'blue',
  Green: 'green'
} as const;

type Color = typeof Color[keyof typeof Color]; // 'red' | 'blue' | 'green'

// as constは以下の点で優れています:
// - より軽量
// - enumの複雑な機能が不要な場合は不要なコードを避けられる
// - オブジェクト型なので拡張が簡単

注意3:enumメンバーのシリアライズ

JSONに変換する際、enumが正しく変換されるか確認が必要です。

enum Status {
  Active = 'active',
  Inactive = 'inactive'
}

interface User {
  id: number;
  name: string;
  status: Status;
}

const user: User = {
  id: 1,
  name: 'John',
  status: Status.Active
};

// JSON.stringifyでは問題ない
console.log(JSON.stringify(user));
// 出力:{\"id\":1,\"name\":\"John\",\"status\":\"active\"}

// しかし逆変換時には型チェックが必要
const json = '{\"id\":1,\"name\":\"John\",\"status\":\"invalid\"}';
const parsed = JSON.parse(json);

// 安全に変換する
function parseUserStatus(value: any): Status | null {
  return Object.values(Status).includes(value) ? value : null;
}

注意4:enumの拡張は困難

一度定義したenumに新しい値を追加する場合、既存の分岐処理に影響が出ます。これは長期的には管理が難しくなることもあります。

// 初期状態
enum OrderStatus {
  Pending = 'pending',
  Shipped = 'shipped',
  Delivered = 'delivered'
}

// 後から新しい値を追加
enum OrderStatus {
  Pending = 'pending',
  Processing = 'processing', // 新規追加
  Shipped = 'shipped',
  Delivered = 'delivered'
}

// 既存のswitch文は警告される(すべてのケースをカバーしていないため)
function handleStatus(status: OrderStatus) {
  switch (status) {
    case OrderStatus.Pending:
      // ...
      break;
    case OrderStatus.Shipped:
      // ...
      break;
    case OrderStatus.Delivered:
      // ...
      break;
    // Processing のケースが漏れて警告される
  }
}

実務でのベストプラクティス

プラクティス1:enumとバリデーションの組み合わせ

ユーザー入力をenumに変換する際は、必ずバリデーションを行いましょう。

// Zod等のバリデーションライブラリを活用
import { z } from 'zod';

enum OrderStatus {
  Pending = 'pending',
  Confirmed = 'confirmed',
  Shipped = 'shipped'
}

const orderStatusSchema = z.nativeEnum(OrderStatus);

// ユーザー入力を安全に変換
function updateOrderStatusFromAPI(status: unknown): OrderStatus {
  return orderStatusSchema.parse(status);
}

// 使用例
try {
  const status = updateOrderStatusFromAPI('invalid_status'); // エラー
} catch (error) {
  console.error('Invalid status received');
}

プラクティス2:enumのドキュメント化

enumのメンバーが何を意味するかをコメントで明確に記述します。

/**
 * ユーザーのサブスクリプション状態
 * 状態遷移:Free -> Premium -> Cancelled(Free に戻ることはない)
 */
enum SubscriptionStatus {
  /** 無料プラン */
  Free = 'free',
  /** 有料プラン(アクティブ) */
  Premium = 'premium',
  /** キャンセル済み */
  Cancelled = 'cancelled',
  /** 支払い期限切れ */
  Suspended = 'suspended'
}

/**
 * 利用可能な支払い方法
 * 各ゲートウェイの制限は paymentGatewayConfig を参照
 */
enum PaymentGateway {
  Stripe = 'stripe',
  PayPal = 'paypal'
}

まとめ

TypeScriptのenumは、適切に使うことで実務コードの品質を大幅に向上させる機能です。本記事で紹介したポイントをまとめます:

  • 文字列enumを使う:数値enumより可読性に優れている
  • 型安全性を活用する:コンパイル時のチェックで実行時エラーを防ぐ
  • enumの関連データを管理する:enumとメタデータを組み合わせて実務的な機能を実装する
  • バリデーションとセットで使う:外部からの入力は必ずチェックする
  • ドキュメントを付ける:チームメンバーが共通理解できるようにする
  • 拡張性を考える:新しい値の追加時の影響を予測する

大規模なプロジェクトではenumを活用することで、保守性が高く、バグの少ないコードベースが実現できます。ぜひ実務に取り入れてみてください。

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