TypeScriptでenumを使わない理由は、enumが「型を消すだけでは動かない、JavaScriptに無い構文」だからです。 TypeScript 5.8で追加された erasableSyntaxOnly オプションや、Node.js 22.18以降で既定になった型ストリッピング(型注釈を取り除くだけで .ts を直接実行する仕組み)では、enumはエラーになります。代わりに union型("todo" | "done")や as const オブジェクト+satisfies を使うと、型の安全性はそのままに、型を消すだけで動くコードになります。

この記事では、TypeScript 7.0.2(npmの最新版)・6.0.3・5.9.3 と Node.js 22.22.2 で実際にenumを実行・型チェックし、出たエラー文をそのまま載せています。数値enum・文字列enum・const enumとunion型・as const オブジェクトの比較表、tscが出力するJavaScriptの違い、コピペで使える置き換えテンプレート(typeof X[keyof typeof X]、satisfies Record、never による網羅性チェック)、enumを使ってよい場面の判断表と段階的な移行手順まで、この1本で確認できます。type と interface の違い、ジェネリクス、ユーティリティ型はそれぞれ別記事で扱っています。

enumとunion型の早見表

比べる点 数値enum 文字列enum const enum union型 as const オブジェクト
書き方 enum D { Up, Down } enum S { Draft = "draft" } const enum P { Low = 1 } type S = "draft" | "published" const S = { Draft: "draft" } as const
JSに出力されるもの 即時関数+逆引き 即時関数 値が埋め込まれる 何も出ない ただのオブジェクト
erasableSyntaxOnly TS1294 エラー TS1294 エラー TS1294 エラー OK OK
Node.js 22 で .ts を直接実行 SyntaxError SyntaxError SyntaxError 動く 動く
文字列リテラルを代入 ― "draft" は代入不可 ― 代入できる 型を作れば代入できる
Object.values で一覧 名前も混ざる 値の一覧 使えない 使えない(型だけ) 値の一覧
S.Draft のような参照 できる できる できる できない できる
おすすめ度(新しいコード) 用途を絞って 置き換え推奨 置き換え推奨 選択肢だけなら最適 値も一覧も欲しいなら最適

迷ったら「選択肢を型にするだけなら union型、Status.Draft のような参照や値の一覧も欲しいなら as const オブジェクト+同名の型」 を選ぶと、enumでできていたことはほぼ置き換えられます。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「TypeScript enumを使わない理由|union型とsatisfiesで置換」

検証環境

この記事のエラー文と出力は、次の環境で実際に実行して取得しました。

ツール バージョン 備考
TypeScript 7.0.2(npm の latest)、6.0.3、5.9.3 3つとも同じ設定で比較
Node.js 22.22.2 型ストリッピングが既定で有効(22.18.0から)
tsconfig target: es2022、strict: true module は出力確認で esnext、型チェックで nodenext
npm i -D ts7@npm:typescript@7.0.2 ts6@npm:typescript@6 ts59@npm:typescript@5.9
node node_modules/ts7/bin/tsc -v   # → Version 7.0.2
node node_modules/ts6/bin/tsc -v   # → Version 6.0.3
node node_modules/ts59/bin/tsc -v  # → Version 5.9.3

TypeScript 7 でもenumは引き続き書けて、出力されるJavaScriptは5.9と同じでした(diff で差分なし)。 「enumが廃止された」のではなく、「型を消すだけで動く書き方に揃える選択肢が公式に用意された」と理解するのが正確です。

enumを使わない理由1:erasableSyntaxOnly でエラーになる

erasableSyntaxOnly は、型注釈を取り除くだけでJavaScriptになる構文(erasable=消せる構文)以外を禁止するオプションです。TypeScript 5.8で追加されました。

// src/enums.ts
export enum Direction { Up, Down }
export enum Status { Draft = "draft", Published = "published" }
export const enum Priority { Low = 1, High = 2 }
npx tsc -p . --erasableSyntaxOnly --noEmit

実行結果(TypeScript 7.0.2。6.0.3・5.9.3でも同じ3行):

src/enums.ts(1,13): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
src/enums.ts(2,13): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
src/enums.ts(3,19): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.

数値enum・文字列enum・const enumのすべてがエラーになります。 enum以外にも、次の構文が対象です(TypeScript 7.0.2で確認)。

構文 erasableSyntaxOnly 置き換え
enum / const enum TS1294 union型、as const オブジェクト
値を持つ namespace TS1294 ESモジュール(export)
コンストラクタ引数プロパティ constructor(private name: string) TS1294 フィールドを宣言して代入
型だけの namespace、declare enum OK ―

tsconfigでは次のように有効にします。

{
  "compilerOptions": {
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true
  }
}

enumを使わない理由2:Node.js の型ストリッピングで動かない

Node.js 22.18.0以降(22.22.2で確認)は、フラグなしで .ts ファイルを直接実行できます。ただし行うのは「型を取り除くこと」だけなので、JavaScriptへの変換が必要なenumは実行できません。

node src/enums.ts

実行結果(Node.js 22.22.2、スタックトレースは省略):

export enum Direction { Up, Down }
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^

SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode

一方、as const オブジェクトとunion型で書いたファイルはそのまま動きます。

node src/union.ts
# → published [ 'draft', 'published' ]

node --experimental-transform-types を付けるとenumも実行できますが、ExperimentalWarning: Transform Types is an experimental feature and might change at any time という警告が出ます。 試験的な機能に頼らず、erasableSyntaxOnly を有効にしてNode.jsの型ストリッピングと同じルールで型チェックしておくと、「tscは通るのにNode.jsで動かない」というずれを防げます。同じ .ts を、Node.jsのほか、型を取り除くだけで動かす各種ツールでもそのまま扱えるのが利点です。

コンストラクタ引数プロパティや値を持つ namespace も、Node.js 22.22.2で同様に TypeScript parameter property is not supported in strip-only mode、TypeScript namespace declaration is not supported in strip-only mode となりました。

enumを使わない理由3:tscの出力と型の動きがunionと違う

tscが出力するJavaScript(並べて比較)

// 入力:enumで書いた場合
export enum Direction { Up, Down }
export enum Status { Draft = "draft", Published = "published" }
export const enum Priority { Low = 1, High = 2 }
export const p = Priority.High;
// 出力(TypeScript 7.0.2 / 5.9.3 で同一、target: es2022, module: esnext)
export var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Down"] = 1] = "Down";
})(Direction || (Direction = {}));
export var Status;
(function (Status) {
    Status["Draft"] = "draft";
    Status["Published"] = "published";
})(Status || (Status = {}));
export const p = 2 /* Priority.High */;
// 入力:as const オブジェクト+union型で書いた場合
export const Status = { Draft: "draft", Published: "published" } as const;
export type Status = (typeof Status)[keyof typeof Status];
export type Direction = "up" | "down";
const s: Status = Status.Published;
// 出力
export const Status = { Draft: "draft", Published: "published" };
const s = Status.Published;

enumは即時関数(IIFE)に変換され、as const は型の部分が消えるだけで、書いたオブジェクトがそのまま残ります。 union型(Direction)は出力に何も残りません。

数値enumには逆引きが入る

// 上の出力を実行
console.log(Direction.Up, Direction[0], Status.Published, p);
// → 0 Up published 2
console.log(Object.keys(Direction), Object.keys(Status));
// → [ '0', '1', 'Up', 'Down' ] [ 'Draft', 'Published' ]

数値enumは「名前→値」と「値→名前」の両方が入るため、Object.keys で一覧を作ると '0' や '1' が混ざります。 文字列enumに逆引きはありません。

型チェックの動きの違い(実際のエラー文)

enum Direction { Up, Down }
enum Status { Draft = "draft", Published = "published" }
enum Status2 { Draft = "draft" }

const d1: Direction = 5;          // ①
const num: number = 5;
const d2: Direction = num;        // ② number型の変数は代入できる
const s1: Status = "draft";       // ③
const same = Status.Draft === Status2.Draft; // ④

実行結果(TypeScript 7.0.2、5.9.3も同じ):

a.ts(4,7): error TS2322: Type '5' is not assignable to type 'Direction'.
a.ts(7,7): error TS2322: Type '"draft"' is not assignable to type 'Status'.
a.ts(8,14): error TS2367: This comparison appears to be unintentional because the types 'Status' and 'Status2' have no overlap.
確認したこと 結果 意味
① 数値enumに範囲外のリテラル 5 エラー TypeScript 5.0以降はリテラルを検査
② 数値enumに number 型の変数 エラーにならない 範囲外の値が入る余地がある
③ 文字列enumに "draft" エラー 値が同じでも文字列は代入できない
④ 同じ値を持つ別のenumの比較 エラー enumは名前で区別される

文字列enumは、APIのレスポンスやフォームの値(ただの文字列)をそのまま代入できないため、変換や型アサーションが増えがちです。 union型なら "draft" をそのまま代入でき、間違った文字列だけがエラーになります。

置き換えテンプレート(コピペ用)

テンプレート1:選択肢だけなら union型

export type Direction = "up" | "down" | "left" | "right";

function move(d: Direction) { /* ... */ }
move("up");     // OK
move("upper");  // error TS2345(引数の型エラー)

値の一覧や Direction.Up のような参照が不要なら、union型だけで十分です。 出力のJavaScriptには何も残りません。

テンプレート2:値も一覧も欲しいなら as const + 同名の型

// task-status.ts
export const TaskStatus = {
  Todo: "todo",
  Doing: "doing",
  Done: "done",
} as const;
export type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus];
// → "todo" | "doing" | "done"

const ok: TaskStatus = "todo";          // OK(文字列をそのまま代入できる)
const ok2: TaskStatus = TaskStatus.Done; // OK(enumと同じ書き方も使える)
const ng: TaskStatus = "finished";
// → error TS2322: Type '"finished"' is not assignable to type 'TaskStatus'.
部品 役割
as const 値を string ではなく "todo" のようなリテラル型にし、読み取り専用にする
typeof TaskStatus オブジェクトの型を取り出す
keyof typeof TaskStatus キーのunion("Todo" | "Doing" | "Done")
(typeof TaskStatus)[keyof typeof TaskStatus] 値のunion("todo" | "doing" | "done")

値(const)と型(type)に同じ名前を付けられるので、TaskStatus.Done という呼び出し側の書き方を変えずにenumから移行できます。

テンプレート3:表示名などの対応表は satisfies Record

export const statusLabel = {
  todo: "未着手",
  doing: "作業中",
} satisfies Record<TaskStatus, string>;

実行結果(done を書き忘れた場合):

// TypeScript 7.0.2
error TS2741: Property 'done' is missing in type '{ todo: string; doing: string; }' but required in type 'Record<TaskStatus, string>'.

// TypeScript 5.9.3
error TS1360: Type '{ todo: string; doing: string; }' does not satisfy the expected type 'Record<TaskStatus, string>'.
  Property 'done' is missing in type '{ todo: string; doing: string; }' but required in type 'Record<TaskStatus, string>'.

余分なキーを書いた場合:

error TS2353: Object literal may only specify known properties, and 'archived' does not exist in type 'Record<TaskStatus, string>'.

satisfies は「この型の条件を満たしているか」だけを検査し、変数の型そのものは書いた値から推論されたまま残します。 const statusLabel: Record<TaskStatus, string> = ... と型注釈で書くと、型が Record<...> に広がりますが、satisfies なら値に近い型が保たれます。as const satisfies Record<TaskStatus, string> とすると、値も "未着手" のようなリテラル型になります。選択肢を追加したときに、対応表の書き忘れがすべてエラーとして見つかるのが最大の利点です。

テンプレート4:switch の網羅性チェック(never)

export function color(s: TaskStatus): string {
  switch (s) {
    case "todo": return "gray";
    case "doing": return "blue";
    default: {
      const _exhaustive: never = s;
      return _exhaustive;
    }
  }
}

実行結果(case "done" を書き忘れた場合):

error TS2322: Type '"done"' is not assignable to type 'never'.

すべての case を書くと default に来る値が無くなり s が never 型になるため、書き忘れがあるとそこでエラーになります。 エラー文に漏れている値("done")が出るので、どこを直せばよいかすぐにわかります。

テンプレート5:外部から来た文字列をチェックする型ガード

const ALL = Object.values(TaskStatus);

export function isTaskStatus(v: unknown): v is TaskStatus {
  return typeof v === "string" && (ALL as readonly string[]).includes(v);
}

export const statusLabel = {
  todo: "未着手",
  doing: "作業中",
  done: "完了",
} as const satisfies Record<TaskStatus, string>;

const label = (s: TaskStatus): string => statusLabel[s];

console.log(ALL);
console.log(isTaskStatus("doing"), isTaskStatus("finished"), isTaskStatus(1));
console.log(label(TaskStatus.Done));
const input: string = "todo";
if (isTaskStatus(input)) console.log("ok:", label(input));

node src/demo.ts(Node.js 22.22.2、型ストリッピングでそのまま実行)の結果:

[ 'todo', 'doing', 'done' ]
true false false
完了
ok: 未着手

Object.values で取り出した一覧を使えば、APIやURLパラメータから来た文字列を安全に TaskStatus に絞り込めます。 数値enumのように名前が混ざることもありません。型ガード(v is TaskStatus)の書き方は、「anyとunknownの違い」の記事でも扱っています。

enumを使ってよい場面の判断表

enumが使えないわけではありません。 ビルドの方法やチームの方針によっては、enumのままで問題ない場面もあります。

状況 おすすめ 理由
Node.js で .ts を直接実行する、erasableSyntaxOnly を有効にしている union型 / as const enumはエラーになる
新しく選択肢を定義する union型 / as const 型を消すだけで動き、文字列をそのまま扱える
APIやDBから来る文字列と比較する union型 / as const 変換や型アサーションが不要
ビットフラグ(Read = 1 << 0 など)を組み合わせる 数値enumも選択肢 数値のまとまりとして読みやすい。as const でも書ける
tscやバンドラで必ず変換していて、既存のenumが多い 既存はそのままでもよい 新規分から置き換え、段階的に移行
ライブラリの公開APIがenumを返している 利用側はそのまま使う 外部の型は変えられない
生成ツール(OpenAPIなど)がenumを出力する 生成設定でunion出力に切り替え可能か確認 ツールの設定で選べることが多い
flowchart TD
  A[選択肢を定義したい] --> B{値の一覧や X.Name の参照が必要?}
  B -- いいえ --> C[union型]
  B -- はい --> D{Node.jsの型ストリッピングや erasableSyntaxOnly を使う?}
  D -- はい --> E[as const オブジェクト+同名の型]
  D -- いいえ --> F{既存コードがenumで統一されている?}
  F -- はい --> G[当面はenumのまま、新規はas const]
  F -- いいえ --> E

enumから段階的に移行する手順

一度に全部を書き換えず、「新規はunion」「既存は1つずつ」の順に進めると、差分が小さく確認しやすくなります。

手順 やること 確認方法
1 enumの数と種類を洗い出す grep -rnE "\b(const )?enum " src
2 新しいコードはunion型 / as constで書くとチームで決める レビューで確認
3 文字列enumを as const + 同名の型に置き換える tsc --noEmit が通る
4 数値enumは値を保ったまま置き換える(保存済みの数値と合わせる) 逆引き X[0] の使用箇所を検索
5 const enumを as const に置き換える 出力サイズの差は小さいことを確認
6 erasableSyntaxOnly: true を有効にする TS1294 が0件になる
7 Node.js で .ts を直接実行できるか確かめる node src/main.ts

文字列enumの置き換え(直す前と後)

// 直す前
export enum TaskStatus {
  Todo = "todo",
  Doing = "doing",
  Done = "done",
}

// 直した後(呼び出し側の TaskStatus.Done や型注釈 TaskStatus はそのまま使える)
export const TaskStatus = {
  Todo: "todo",
  Doing: "doing",
  Done: "done",
} as const;
export type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus];

キー名と値をenumと同じにすると、呼び出し側の変更はほぼ不要です。 置き換え後は "todo" のような文字列も代入できるようになるため、以前は必要だった as TaskStatus の型アサーションを外せます。

数値enumの置き換え(逆引きは明示的な表にする)

// 直す前
export enum Level { Low, Middle, High } // 0, 1, 2
const name = Level[1]; // "Middle"(逆引き)

// 直した後(保存済みの数値と合わせるため、値を明示する)
export const Level = { Low: 0, Middle: 1, High: 2 } as const;
export type Level = (typeof Level)[keyof typeof Level]; // 0 | 1 | 2

export const levelName = {
  0: "Low",
  1: "Middle",
  2: "High",
} as const satisfies Record<Level, string>;
const name2 = levelName[1]; // "Middle"

数値enumは値が自動採番されるので、置き換えるときは値を必ず明示します。 逆引きを使っていた箇所は、satisfies Record で書いた対応表に置き換えると、項目の追加漏れも型チェックで見つかります。

よくある質問

Q. TypeScript 7ではenumが使えなくなったのですか? A. いいえ、TypeScript 7.0.2でもenumは書けて、出力されるJavaScriptも5.9.3と同じでした。 エラーになるのは erasableSyntaxOnly を有効にした場合と、Node.jsの型ストリッピングで直接実行した場合です。

Q. union型と as const オブジェクトはどちらを選べばよいですか? A. 型として選択肢を表したいだけならunion型、値の一覧(セレクトボックスの選択肢など)や X.Name の参照も欲しいなら as const オブジェクトです。 as const オブジェクトから typeof X[keyof typeof X] でunion型を作れるので、両方を1か所で管理できます。

Q. satisfies と型注釈(: Record<...>)の違いは何ですか? A. どちらも型の条件を満たすか検査しますが、satisfies は変数の型を書いた値のまま残し、型注釈は指定した型に広げます。 対応表のキーの漏れを検査しつつ、値のリテラル型も活かしたいときは as const satisfies Record<...> が便利です。

Q. const enum なら出力に残らないので使ってもよいですか? A. const enumも erasableSyntaxOnly ではTS1294になり、Node.jsの型ストリッピングでも実行できません。 値を埋め込むためにTypeScriptの変換が必要だからです。as const オブジェクトは出力に小さなオブジェクトが残りますが、型を消すだけで動きます。

まとめ

  • enumは型を消すだけでは動かない構文で、erasableSyntaxOnly ではTS1294、Node.js 22の型ストリッピングでは ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX になる
  • TypeScript 7.0.2でもenumは書け、出力JSは5.9.3と同じ。オプションと実行方法しだいで使い分ける
  • 選択肢だけならunion型、値の一覧や参照も欲しいなら as const + typeof X[keyof typeof X]
  • 対応表は satisfies Record<型, 値> で書くと、選択肢を増やしたときの書き忘れがエラーになる
  • switchは const _exhaustive: never = s で網羅性チェック。漏れた値がエラー文に出る
  • 数値enumは逆引きが混ざるため、置き換えでは値を明示し、逆引きは対応表にする
  • 移行は「新規はunion」「既存は1つずつ」→ 最後に erasableSyntaxOnly を有効化 の順で進める

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「TypeScript enumを使わない理由|union型とsatisfiesで置換」

株式会社RJCでは、TypeScriptの型の設計を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?