こんにちは、かつコーチです。
前回は、type と interface の違いについて解説しました。
今回は、TypeScript独自の機能である「enum(列挙型)」を扱います。
「決まった選択肢の中からどれか一つを選ばせたい」という場面は、実務でとてもよく出てきます。
enumを使うと、その「決まった選択肢」を安全に、かつ読みやすく表現できます。
enumとは?
決まった値のリストを表す型
enum(列挙型) とは、あらかじめ決められた値の集まりに、まとめて名前をつける仕組みです。
たとえば「曜日」や「注文のステータス」のように、取りうる値があらかじめ決まっているものを表すのに向いています。
enum OrderStatus {
Pending,
Processing,
Shipped,
Delivered,
}
const status: OrderStatus = OrderStatus.Pending;
console.log(status); // 0
OrderStatus.Pending のように、決められた名前で値を参照できるのが特徴です。
なぜenumが必要なのか
「文字列や数値で直接表現すればいいのでは?」と思うかもしれません。
実際、私が最初にTypeScriptを学んだときも、なぜわざわざenumという専用の書き方があるのか、しばらくピンときていませんでした。
しかし、次のようなコードを見比べると、enumのありがたみが分かります。
// enumを使わない場合:どんな値が来るか分からない
function updateStatus(status: string) {
if (status === "pending") {
// ...
}
}
updateStatus("pendign"); // タイプミスに気づけない
文字列だけで管理していると、タイプミスがあってもエラーにならず、実行してから初めて不具合に気づくということが起こります。
enumを使えば、こうしたタイプミスをコンパイル時点で防げます。
enumの基本の書き方
数値enum
何も指定しない場合、enumのメンバーには自動的に0から順番に数値が割り振られます。
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
const move: Direction = Direction.Up;
console.log(move); // 0
途中の値を指定すると、その値から続きの番号が振られます。
enum Direction {
Up = 1,
Down, // 2
Left, // 3
Right, // 4
}
文字列enum
数値だけだと「0や1が具体的に何を意味するか」がコードを見ただけでは分かりにくい、というデメリットがあります。
こういった場合は、文字列enumを使うと可読性が上がります。
enum OrderStatus {
Pending = "PENDING",
Processing = "PROCESSING",
Shipped = "SHIPPED",
Delivered = "DELIVERED",
}
const status: OrderStatus = OrderStatus.Shipped;
console.log(status); // "SHIPPED"
デバッグ時にログへ出力される値も文字列になるため、「今どの状態なのか」がひと目で分かりやすくなります。
実務でenumを使う場合は、数値enumより文字列enumが選ばれるケースの方が多い印象です。
const enumとの違い
enum の前に const をつけると、コンパイル後のコードが軽量になる「const enum」になります。
const enum Direction {
Up,
Down,
}
const move = Direction.Up;
通常のenumはコンパイル後もオブジェクトとしてコードに残りますが、const enum はコンパイル時にすべて値へ置き換えられ、実行時のコードには残りません。
パフォーマンスを気にする場面では選択肢に入りますが、一部のビルドツールでは非対応の場合もあるため、まずは通常のenumから使い始めるのがおすすめです。
enumを使う実践例
注文ステータスの管理
実際にenumを使ったコード例を見てみましょう。
enum OrderStatus {
Pending = "PENDING",
Processing = "PROCESSING",
Shipped = "SHIPPED",
Delivered = "DELIVERED",
}
function getStatusLabel(status: OrderStatus): string {
switch (status) {
case OrderStatus.Pending:
return "注文受付中";
case OrderStatus.Processing:
return "処理中";
case OrderStatus.Shipped:
return "発送済み";
case OrderStatus.Delivered:
return "配達完了";
}
}
console.log(getStatusLabel(OrderStatus.Shipped)); // "発送済み"
switch 文とenumを組み合わせると、「取りうる値がすべて決まっている」という前提のもとで、漏れのない分岐を書けます。
関数の引数をenumで制限する
enumを引数の型として使うと、想定外の値が渡されるのをコンパイル時に防げます。
❌ Before:文字列で受け取ってしまう
function setDirection(direction: string) {
console.log(`移動方向: ${direction}`);
}
setDirection("up");
setDirection("upp"); // タイプミスでもエラーにならない
✅ After:enumで受け取る値を制限する
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
function setDirection(direction: Direction) {
console.log(`移動方向: ${direction}`);
}
setDirection(Direction.Up);
// setDirection("upp"); // ❌ コンパイルエラーになる
Afterのコードでは、Direction 型で定義された値以外を渡すとコンパイルエラーになるため、タイプミスをその場で発見できます。
つまずきやすいポイント
数値enumで意図しない値が入り込む
私が実際に業務でつまずいたのが、数値enumの「型の緩さ」です。
数値enumは、実は数値そのものを直接代入できてしまいます。
enum Direction {
Up,
Down,
}
const move: Direction = 99; // エラーにならない(意図しない値が入る)
Direction に定義されていない 99 という値でも、数値enumの場合はコンパイルエラーにならずに通ってしまいます。
これに気づかず、APIから返ってきた数値をそのまま Direction 型の変数に代入してしまい、想定外のケースでバグを起こしたことがありました。
文字列enumであればこの問題は起きないため、迷ったら文字列enumを使うのが安全です。
enum Direction {
Up = "UP",
Down = "DOWN",
}
const move: Direction = "UNKNOWN"; // ❌ こちらはちゃんとエラーになる
enumとユニオン型リテラルの使い分け
TypeScriptには、enumと似た目的で使える「リテラル型のユニオン」という書き方もあります。
type Direction = "UP" | "DOWN" | "LEFT" | "RIGHT";
const move: Direction = "UP";
どちらも「決まった値の中から選ばせる」という点は同じですが、性質が少し違います。
| 観点 | enum | リテラル型のユニオン |
|---|---|---|
| 実行時のコード | オブジェクトとして残る | 残らない(型情報のみ) |
| バンドルサイズへの影響 | ややある | ない |
| 値の一覧をループで取得 | しやすい | 別途配列が必要 |
| チームでの馴染みやすさ | Java・C#経験者に馴染みやすい | JavaScriptに近い感覚 |
最近のフロントエンド開発では、バンドルサイズや実行時コストの観点から、リテラル型のユニオンを好むチームも増えています。
どちらを使うかはプロジェクトの方針次第ですが、まずはenumの基本を押さえておくと、両方の書き方を状況に応じて選べるようになります。
まとめ
この記事のポイント
- enumは「決まった選択肢」を安全に表現するためのTypeScript独自の機能
- 数値enumは自動で番号が振られ、文字列enumは値が読みやすい
- 数値enumは想定外の数値が代入できてしまうため、迷ったら文字列enumを使う
- enumと似た用途でリテラル型のユニオンも使われる
- switch文と組み合わせると、漏れのない分岐処理を書ける
次に読むべき記事
次回は、オプショナルプロパティ(?)と readonly の使い方について解説します。
→ 次の記事:オプショナルプロパティ(?)とreadonlyの使い方