コードレビューでの一幕:その `enum`、本当に必要ですか?
プルリクエストのレビュー中、次のようなコードを見かけて手が止まったことはないでしょうか。
// よく見かける従来の書き方
enum Role {
ADMIN = ‘ADMIN’,
EDITOR = ‘EDITOR’,
VIEWER = ‘VIEWER’,
}
function processUser(role: Role) {
// …
}
一見して何の問題もないように思えるかもしれません。しかし、TypeScriptの型システムとコンパイルのメカニズムを深く理解しているアーキテクトの視点から言えば、この `enum` の使用には保守性、バンドルサイズ、そして型安全性における致命的な見落としが潜んでいます。
本記事では、TypeScriptの `enum` が抱える構造的な欠陥を暴き、その圧倒的な代替手段である `as const`(Const Assertions)オブジェクト が、なぜモダンなフロントエンド開発のデファクトスタンダードであるべきなのかを、コンパイラの挙動を交えて徹底解説します。
—
なぜ `enum` はTypeScriptの異端児なのか
TypeScriptを使いこなす上で知っておくべき最大の事実があります。それは、`enum` はTypeScriptの仕様の中で、数少ない「独自のJavaScriptコードを生成する機能」であるということです。
ほとんどのTypeScriptの機能(interfacesやtype aliasesなど)は、コンパイル時にきれいに消え去り、JavaScriptのランタイムには影響を与えません。しかし、通常の `enum` は以下のようなJavaScriptの即時関数(IIFE)を出力します。
// コンパイル後のJavaScript (一部簡略化)
var Role;
(function (Role) {
Role[“ADMIN”] = “ADMIN”;
Role[“EDITOR”] = “EDITOR”;
Role[“VIEWER”] = “VIEWER”;
})(Role || (Role = {}));
この実装には、以下の実務上のリスクが伴います。
1. バンドルサイズの肥大化: 使用していない `enum` であっても、ツリーシェイキング(Dead Code Elimination)が効きにくく、ビルド成果物に不要なコードが残り続ける。
2. JavaScriptのネイティブエコシステムとの乖離: 他のライブラリやJSのコードベースとの相互運用において、余計なオブジェクトのインポートや型変換が必要になる。
3. 数値型enumの危険な暗黙の挙動: 値を明示しない場合、数値が割り振られ、意図しない数値が代入できてしまう脆弱性がある。
—
解法:`as const` オブジェクトによる圧倒的な型安全性
これらの課題に対するエレガントかつ堅牢な答えが、`as const`(Const Assertions)を用いたオブジェクト定義です。
まずは、プロダクションコードでそのまま使える理想的な設計パターンを見てみましょう。
実践:プロダクションレベルの型設計
/
- 権限管理を例にした、拡張性と安全性の高い定義
- as constにより、プロパティが再代入不可かつリテラル型として推論される
/
export const Role = {
Admin: ‘ADMIN’,
Editor: ‘EDITOR’,
Viewer: ‘VIEWER’,
} as const;
/
- 型エイリアスの抽出
- typeof と keyof を組み合わせることで、オブジェクトから正確な型を導出する
/
export type RoleKey = keyof typeof Role; // ‘Admin’ | ‘Editor’ | ‘Viewer’
export type RoleValue = typeof Role[RoleKey]; // ‘ADMIN’ | ‘EDITOR’ | ‘VIEWER’
/
- 【実務パターン】API連携やコンポーネントのプロパティ設計
/
interface User {
id: string;
name: string;
role: RoleValue; // 厳格に ‘ADMIN’ | ‘EDITOR’ | ‘VIEWER’ のみが許可される
}
/
- 権限に応じた処理を安全に行う関数
- TypeScriptの網羅性チェック(Exhaustive Check)と組み合わせることでバグをコンパイル時に検知
/
export function getRoleDisplayName(role: RoleValue): string {
switch (role) {
case Role.Admin:
return ‘システム管理者’;
case Role.Editor:
return ‘編集者’;
case Role.Viewer:
return ‘閲覧者’;
default:
// 網羅性チェック: 将来RoleValueに新しい値が追加された際、
// ここをハンドリングし忘れるとコンパイルエラーになる
const _exhaustiveCheck: never = role;
throw new Error(`未定義のロールです: ${_exhaustiveCheck}`);
}
}
—
なぜ `as const` が優れているのか?(3つのコア優位性)
1. ゼロ・ランタイムコスト(バンドルへの影響ゼロ)
`as const` で定義されたオブジェクトは、通常のJavaScriptのオブジェクト(あるいは完全にインライン展開される定数)として扱われます。不要なIIFEが生成されないため、モジュールバンドラーのツリーシェイキングが100%機能し、バンドルサイズを極限までクリーンに保てます。
2. キーと値の完全な分離と柔軟性
`enum` は「値からキーへの逆引き(数値enumの場合)」などの特殊な挙動を持ちますが、多くの場合、実務で必要なのは「キーの型」と「値の型」をそれぞれ独立して扱うことです。
`as const` であれば、`keyof typeof` と `typeof Obj[keyof typeof Obj]` を使うことで、ユースケースに応じた型を自由自在に生成できます。
3. イミュータビリティの保証
`as const` は、オブジェクトのすべてのプロパティを `readonly` にし、さらにプリミティブ値をリテラル型(例: `’ADMIN’` そのもの)として推論させます。これにより、誤ってコード内で値を書き換えるバグをコンパイル時に完全に阻止できます。
—
現場のチーフアーキテクトからの提言
新規にTypeScriptプロジェクトを立ち上げる際、あるいは既存のコードベースをリファクタリングする際は、「迷ったら `enum` は使わず `as const` を採用する」をチームのコーディング規約の基本方針に据えてください。
`enum` は歴史的な経緯からTypeScriptに残された機能であり、現代の高度な型推論システム(TypeScript 3.4以降で導入された `as const`)の前では、その存在意義はほぼ失われています。
型安全性、パフォーマンス、そしてJavaScriptエコシステムとの親和性――すべてにおいて優位な `as const` オブジェクトを使いこなし、コンパイラをあなたの最強の味方にしましょう。