- TypeScript
TypeScript enumを使わない理由|union型とsatisfiesで置換
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期連続の黒字決算。