数値enumはObject.keys()で要素数が2倍になる(逆引きマッピングの副作用)
数値enumは、コンパイル後のJavaScriptオブジェクトに「メンバー名→値」だけでなく「値→メンバー名」の逆引きも自動的に持たせます。この副作用でObject.keys()を呼ぶと、メンバーの2倍の数のキーが返ってきます。
なぜエラーが出ないのか
出力: 4
(エラーなし)- TypeScriptは何も報告しません。型検査も構文も正しいためです
出力: 4- 実際の挙動 — 期待した結果と食い違っている箇所
見つけ方- エラーが出ないので、出力を目で確かめるか、コードを目で追うしかありません。この種の誤りが最も発見が遅れます
このエラーが出る典型パターン
パターン1
1 enum Status { 2 Active, 3 Inactive, 4 } 5 console.log(Object.keys(Status).length ); ^
数値enumはコンパイル後、メンバー名→値だけでなく値→メンバー名の逆引きも持ちます。Object.keys().lengthはメンバー数の2倍(4)になります。
直し方: length を filter(k => Number.isNaN(Number(k))).length にします。
パターン2
1 enum Level { 2 Low, 3 Mid, 4 High, 5 } 6 console.log(Object.keys(Level).length ); ^
Levelは3メンバーですが、逆引きマッピングの分も合わせてObject.keys().lengthは6になります。
直し方: length を filter(k => Number.isNaN(Number(k))).length にします。
パターン3
1 enum Direction { 2 North, 3 South, 4 East, 5 West, 6 } 7 console.log(Object.keys(Direction).length ); ^
Directionは4メンバーです。逆引き分も含めるとObject.keys().lengthは8になります。
直し方: length を filter(k => Number.isNaN(Number(k))).length にします。
パターン4
1 enum Weekday { 2 Mon, 3 Tue, 4 Wed, 5 Thu, 6 Fri, 7 } 8 console.log(Object.keys(Weekday).length ); ^
数値enumは逆引きマッピングも持つため、Object.keys().lengthはメンバー数の2倍(10)になります。
直し方: length を filter(k => Number.isNaN(Number(k))).length にします。
パターン5
1 enum Suit { 2 Clubs, 3 Diamonds, 4 Hearts, 5 Spades, 6 } 7 console.log(Object.keys(Suit).length ); ^
Suitは4メンバーです。逆引き分も含めるとObject.keys().lengthは8になります。
直し方: length を filter(k => Number.isNaN(Number(k))).length にします。
よくある誤解
「enumはただのメンバー一覧を表す型のはず」という理解は、コンパイル後の実体までは説明していません。数値enumはコンパイル時にJavaScriptの通常のオブジェクトへ変換され、逆引き用のキーも一緒に埋め込まれます。
実務での勘所
実際にコンパイルすると、enum Color { Red, Green }は{ 0: "Red", 1: "Green", Red: 0, Green: 1 }というオブジェクトになります。名前から値を引く通常の使い方だけでなく、値から名前を引きたい場面(ログ出力でColor[0]から"Red"を得るなど)のために、TypeScriptが両方向の対応を1つのオブジェクトに詰め込んでいます。この副作用を避けたい場合は、文字列enumを使うと、逆引き用のエントリは生成されずObject.keys()も本来の要素数どおりになります。