TypeScriptのEnum型とリテラル型を分かりやすく解説

今日のテーマ:選択肢を限定したい時に使う型

先生、HTMLのセレクトボックスみたいに、決まった選択肢の中からしか値を選べないようにしたい時って、TypeScriptではどうすればいいんですか?

そんな時に使えるのがEnum型リテラル型という2つの方法があるんだ。まずはEnum型から見てみよう。

Enum型とは?

Enumは『enumeration(列挙)』の略で、日本語では『列挙型』と呼ばれるんだ。決まった値の集合を定義できる型だよ。

enumって他のプログラミング言語でも聞いたことがあります!

そうそう。でも、TypeScriptのenumはちょっと注意が必要なんだ。まずは基本的な使い方を見てみよう。

// ゲームのキャラクタータイプを定義
enum CharacterType {
  Warrior,  // 0
  Mage,     // 1
  Archer    // 2
}

// 使い方
console.log(CharacterType.Warrior); // 0
console.log(CharacterType.Mage);    // 1
console.log(CharacterType.Archer);  // 2

// 変数に代入
let playerType: CharacterType = CharacterType.Warrior;
console.log(playerType); // 0

え!実際の値は数値なんですか?

そうなんだ。これが問題の始まりなんだよ。見てみて。

数値Enumの問題点

enum CharacterType {
  Warrior,  // 0
  Mage,     // 1
  Archer    // 2
}

let playerType: CharacterType = CharacterType.Warrior;

// 普通の使い方
playerType = CharacterType.Mage; // OK
console.log(playerType); // 1

// 問題:直接数値を代入してもエラーにならない!
playerType = 99; // エラーにならない!これは危険
console.log(playerType); // 99

// 存在しない値でも受け入れてしまう
playerType = -5; // これもエラーにならない

うわあ、これは困りますね!99とか-5とか、全然意味のない値が入っちゃってる!

そう。これは型安全性が保証されていないということなんだ。でも、文字列Enumを使えばこの問題は解決できるよ。

文字列Enum(改良版)

// 文字列Enumで安全に定義
enum CharacterType {
  Warrior = 'warrior',
  Mage = 'mage',
  Archer = 'archer'
}

let playerType: CharacterType = CharacterType.Warrior;
console.log(playerType); // 'warrior'

// 型安全性が保たれる
playerType = CharacterType.Mage; // OK

// 直接文字列を代入しようとするとエラー
// playerType = 'warrior'; // エラー!
// playerType = 'ninja';   // エラー!

おお!今度はちゃんとエラーになってますね。でも、なぜ ‘warrior’ の代入もダメなんですか?

これが型安全性というものなんだ。文字列 ‘warrior’ とEnum型の CharacterType.Warrior は、値は同じでも型が違うんだよ。だから代入できないんだ。

リテラル型の登場

実は、TypeScriptにはもう一つ同じような働きをする型があるんだ。リテラル型というものだよ。

リテラル型?配列リテラルとかオブジェクトリテラルの『リテラル』ですか?

ちょっと紛らわしいんだけど、ここでは『literal(文字通りの、そのままの)』という意味なんだ。特定の値そのものを型として使うんだよ。

リテラル型の基本

// 特定の文字列だけを許可する型
let gameMode: 'easy' = 'easy'; // 'easy'という文字列のみ許可

// 他の値を代入しようとするとエラー
// gameMode = 'hard'; // エラー!
// gameMode = 'normal'; // エラー!

// 数値リテラル型の例
let maxLevel: 100 = 100; // 100という数値のみ許可
// maxLevel = 99; // エラー!

なるほど!でもこれだと一つの値しか使えないから、選択肢を作るのには向いてませんよね?

そこで登場するのがユニオン型との組み合わせなんだ!

ユニオン型と組み合わせたリテラル型

// ゲーム難易度の選択肢を定義
type GameDifficulty = 'beginner' | 'normal' | 'hard' | 'expert';

let difficulty: GameDifficulty = 'normal'; // OK

// 定義された値のみ使用可能
difficulty = 'beginner'; // OK
difficulty = 'expert';   // OK

// 未定義の値はエラー
// difficulty = 'impossible'; // エラー!
// difficulty = 'easy';       // エラー!

// 実用的な例:HTTPメソッドの定義
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

function sendRequest(method: HttpMethod, url: string) {
  console.log(`${method}リクエストを${url}に送信します`);
}

sendRequest('GET', '/api/users');    // OK
sendRequest('POST', '/api/users');   // OK
// sendRequest('PATCH', '/api/users'); // エラー!

すごい!これなら決まった選択肢の中からしか選べませんね!

Enum型 vs リテラル型:どちらを使うべき?

実は、現在のTypeScriptコミュニティではリテラル型の方が人気なんだ。理由を比較してみよう。

// Enum型の場合
enum Status {
  Loading = 'loading',
  Success = 'success',
  Error = 'error'
}

function handleStatus(status: Status) {
  switch (status) {
    case Status.Loading:
      console.log('読み込み中...');
      break;
    case Status.Success:
      console.log('成功!');
      break;
    case Status.Error:
      console.log('エラーが発生しました');
      break;
  }
}

// リテラル型の場合
type Status = 'loading' | 'success' | 'error';

function handleStatus(status: Status) {
  switch (status) {
    case 'loading':
      console.log('読み込み中...');
      break;
    case 'success':
      console.log('成功!');
      break;
    case 'error':
      console.log('エラーが発生しました');
      break;
  }
}

リテラル型の利点

  1. シンプルな記述:Enum.ValueではなくただのStringが使える
  2. 軽量:JavaScriptにコンパイルした時のコードが短い
  3. 直感的:値そのものを使うので分かりやすい
  4. JSONとの親和性:APIのレスポンスなどと直接マッチする

確かにリテラル型の方がシンプルで分かりやすいですね!

実践的な使い方

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

// ボタンのサイズを限定
type ButtonSize = 'small' | 'medium' | 'large';

// ボタンの種類を限定
type ButtonVariant = 'primary' | 'secondary' | 'danger';

interface ButtonProps {
  size: ButtonSize;
  variant: ButtonVariant;
  children: string;
}

function Button({ size, variant, children }: ButtonProps) {
  const className = `btn btn-${variant} btn-${size}`;
  return <button className={className}>{children}</button>;
}

// 使用例
<Button size="large" variant="primary">送信</Button>
// <Button size="huge" variant="primary">送信</Button> // エラー!

API レスポンスの型定義

// APIの応答ステータスを定義
type ApiStatus = 'pending' | 'fulfilled' | 'rejected';

interface ApiResponse<T> {
  status: ApiStatus;
  data?: T;
  error?: string;
}

// ユーザー情報のレスポンス
const userResponse: ApiResponse<{name: string, age: number}> = {
  status: 'fulfilled', // この値は3つの選択肢に限定される
  data: { name: 'OZ君', age: 17 }
};

const アサーションとの組み合わせ

// 通常の変数宣言
let normalVar = 'hello';  // string型(どんな文字列でもOK)
normalVar = 'world';      // OK

// const宣言
const constVar = 'hello'; // 'hello'リテラル型('hello'のみ)
// constVar = 'world';    // エラー!constなので再代入不可

// 配列やオブジェクトでのconst assertion
const colors = ['red', 'green', 'blue'] as const;
// colorsの型は readonly ['red', 'green', 'blue']

const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
} as const;
// configの各プロパティもリテラル型になる

へえ!const assertionを使うと、配列やオブジェクトの値も固定できるんですね!

まとめ

今日学んだことをまとめてみよう。

ポイント整理

  1. Enum型:列挙型を作れるが、数値Enumは型安全性に問題がある
  2. 文字列Enum:型安全だが、記述が少し冗長
  3. リテラル型 + ユニオン型シンプルで分かりやすく、現在の主流
  4. const assertion:値を固定してリテラル型にできる

実際の開発での選択指針

  • 新しいプロジェクト:リテラル型を推奨
  • 既存のEnumがあるプロジェクト:文字列Enumでも問題なし
  • 選択肢が多い場合:可読性を考慮してEnumも検討

よく分かりました!これからはリテラル型を中心に使ってみます!