TypeScriptの enum は、名前の付いた定数の集合です。enum Direction { Up, Down, Left, Right } と書くと、Direction という型と、Direction.Up のようにメンバーにアクセスできる実行時のオブジェクトの両方が作られます。値を指定しなければメンバーには 0 から順に番号が振られ、文字列 enum では各メンバーに読みやすい文字列を持たせられます。
enum は、単なる型ではないTypeScriptの数少ない機能のひとつです。コードをコンパイルすると、enum は本物のJavaScriptのオブジェクトになります。
数値 enum
初期値を書かなければ、メンバーには 0、1、2 と順に値が付きます。最初のメンバーに数値を指定すると、残りはそこから続きます。すべての値を明示することもでき、数値をデータベースに保存したりネットワークで送ったりする場合はそのほうが安全です。
プログラムの外に出ない値なら自動の番号付けで問題ありません。メンバーの順序が変わる可能性があり、数値をどこかに保存している場合、途中にメンバーを追加するとそれ以降の番号が黙ってすべてずれてしまいます。
enum のコンパイル結果
型は消去されますが、enum は消去されません。数値 enum と文字列 enum に対して、TypeScriptは次のJavaScriptを出力します:
enum Direction { Up, Down, Left, Right }
enum Status { Active = "ACTIVE", Inactive = "INACTIVE" }
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
var Status;
(function (Status) {
Status["Active"] = "ACTIVE";
Status["Inactive"] = "INACTIVE";
})(Status || (Status = {}));
Direction["Up"] = 0 は 0 を返すので、同じ文で Direction[0] = "Up" も設定されます。そのため数値 enum は、名前から数値へ、数値から名前へと双方向に対応します。これが逆マッピングです。文字列 enum は名前から値への対応だけです。
出力された Direction オブジェクトには8つのキーがあります。4つの名前と4つの数値です。ループするときにはこれが問題になります。
文字列 enum
文字列 enum の各メンバーには、明示的な文字列の値が必要です。値はログ、JSON、データベースにそのまま現れるので、数値よりもデバッグしやすくなります。
文字列 enum には意外な面があります。テキストがメンバーの値と一致していても、普通の文字列は代入できません。
index.ts(7,5): error TS2820: Type '"ACTIVE"' is not assignable to type 'Status'. Did you mean 'Status.Inactive'?
(メッセージの提案はコンパイラーの推測で、ここでは間違っています。正しい直し方は Status.Active です。)逆方向では、Status の値は string が期待される場所ならどこでも使えます。JSON やフォームから文字列として値が届く場合は、下の「値が enum に含まれるかチェックする」で紹介するチェックで変換してください。
enum を型として使う
enum の名前は、そのメンバーを値とする型です。switch と組み合わせると、関数が値を返さなければならない場合に、すべてのメンバーが処理されているかをTypeScriptがチェックします:
Shape に新しいメンバーを追加して case を追加しなかった場合、sides は TS2366 Function lacks ending return statement and return type does not include 'undefined'. でコンパイルできなくなります。never を使ったより厳密な網羅性チェックは switch のページで紹介しています。
最後の数行は数値 enum の本当の弱点を示しています。どのメンバーにも一致しない数値リテラル const level: Level = 99 はコンパイルエラー(TS2322)ですが、number 型の値なら何でも受け付けてしまうので、57 は通ってしまいます。文字列 enum にはこの穴はありません。
enum をループする
enum は実行時にはオブジェクトなので、Object.keys、Object.values、Object.entries が使えます。文字列 enum ならちょうどメンバーだけが返ります。数値 enum では逆マッピングのエントリーも返るので、取り除きます:
変数を「enum のメンバー名のどれか」として型付けするには、keyof typeof Direction を使います。これはユニオン "Up" | "Down" | "Left" | "Right" です。そうすれば Direction[name] で型安全に値を取り出せます。
文字列 enum には逆マッピングがないので、値からメンバー名を得るにはエントリーを検索します: Object.entries(Status).find(([, v]) => v === "ACTIVE")?.[0] は "Active" になり、その値を持つメンバーがなければ undefined になります。
値が enum に含まれるかチェックする
プログラムの外から来るデータは普通の string や number です。型ガードでそれを enum の値と照合し、enum の型に絞り込みます:
信頼できない入力に raw as Status を使うのは避けましょう。アサーションはコンパイルが通りますが、実行時には何もチェックされないので、"DELETED" が正しい Status として型付けされたままプログラムを流れていきます。
const enum
const enum は、コンパイラーに enum を削除させ、使っている場所に各メンバーの値を書き込ませます。実行時にはオブジェクトがないので、ループも逆マッピングもできません。
const enum は数バイトとプロパティの参照を節約できますが、それを使うすべてのファイルをコンパイルするときに、コンパイラーが enum の宣言を見られることが前提になります。Babel や swc のようにファイルを1つずつトランスパイルするツールは、別のファイルで宣言された const enum を見られません。Node の型除去は、ほかの enum と同じく const enum も拒否します。isolatedModules や verbatimModuleSyntax を有効にすると、宣言ファイルの const enum を使ったときにTypeScriptはエラー TS2748 を報告します。ほとんどのアプリケーションのコードに const enum は必要ありません。
enum とユニオン型と as const オブジェクト
決まった値の集合を定義する一般的な方法は3つあります:
enum | リテラルのユニオン | as const オブジェクト | |
|---|---|---|---|
| 実行時に存在する | する(オブジェクト) | しない | する(普通のオブジェクト) |
| 値をループする | Object.values(数値はフィルターが必要) | できない(ループする対象がない) | Object.values |
普通の "red" を受け付ける | 受け付けない(文字列 enum) | 受け付ける | 受け付ける |
名前でアクセス X.Red | できる | できない | できる |
| 逆マッピング | 数値 enum のみ | なし | なし |
| Node の型除去で動く | 動かない | 動く | 動く |
erasableSyntaxOnly で許可される | されない | される | される |
| 覚える構文 | enum のルール、const enum | なし | typeof のパターン |
最近は多くのチームが文字列リテラルのユニオンを基本にし、実行時に値が必要なとき(ループしたりドロップダウンを作ったりするとき)に as const オブジェクトに切り替えています。理由は次のとおりです。ユニオンは純粋な型なので出力から消えること、JSON や API が届ける普通の文字列を受け付けること、そして enum が日常的なTypeScriptの中で唯一「JavaScriptに消去できる型を足したもの」ではない部分であることです。
最後の点は実際に影響するようになりました。Node は型を取り除くことで .ts ファイルを直接実行しますが、enum は取り除けるものではありません:
node status.ts
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
Node の --experimental-transform-types フラグを使えば enum も動きます。また、コンパイラーオプション erasableSyntaxOnly はすべての enum をエラー TS1294 This syntax is not allowed when 'erasableSyntaxOnly' is enabled. として報告するので、プロジェクトで最初から enum を禁止できます。型除去のしくみは TypeScriptの実行 を参照してください。だからといって enum が間違っているわけではありません。tsc やバンドラーでコンパイルしたコードでは問題なく動きますし、すでに enum を使っているコードベースを書き換えても得るものはわずかです。
よくある質問
TypeScriptの enum とは何ですか?
型でもあり実行時のオブジェクトでもある、名前の付いた定数の集合です。enum Direction { Up, Down } と書くと Direction.Up と書けるようになり、Direction をパラメーターの型としても使えます。TypeScriptのほとんどの機能と違い、enum は消去されません。実行時に存在するJavaScriptのオブジェクトにコンパイルされます。
TypeScriptで enum をループするには?
文字列 enum なら、Object.values(MyEnum) で値が、Object.keys(MyEnum) で名前が得られます。数値 enum には逆マッピングのエントリー("0": "Up")も含まれるので、取り除きます: Object.keys(Direction).filter((k) => isNaN(Number(k))) で名前だけが得られます。const enum は実行時に存在しないのでループできません。
TypeScriptで文字列を enum の値に変換するには?
型ガードの中で文字列を enum の値と照合します: function isStatus(s: string): s is Status { return (Object.values(Status) as string[]).includes(s); }。チェックのあとは s の型が Status になります。単なる s as Status はコンパイルは通りますが、実行時には何もチェックしません。
TypeScriptでは enum とユニオン型のどちらを使うべきですか?
多くのチームは文字列リテラルのユニオン(type Status = "active" | "inactive")を好み、実行時にも値が必要なときは as const オブジェクトを使います。ユニオンは完全に消去され、Node の組み込みの型除去や erasableSyntaxOnly オプションとも両立し、"active" のような普通の文字列を受け付けます。enum が悪いわけではなく、すでに enum を使っているコードベースならそのままで問題ありません。
enum と const enum の違いは何ですか?
普通の enum は、実行時にループしたり参照したりできるオブジェクトにコンパイルされます。const enum はコンパイル時に取り除かれ、使っている箇所はすべて値に置き換えられます(Size.Large は 2 になります)。実行時のコストはありませんが、ループはできず、ファイルを1つずつコンパイルするツールでは使える場面が制限されます。