Gyors kvíz: hány kulcsa van futásidőben annak az objektumnak, amit a TypeScript egy négytagú enumból generál? Aki nyolcat mondott, az vagy nagyon figyelt, vagy már debuggolt olyan kódot, ami Object.keys()-szel iterált egy enumon, és minden ciklus kétszer futott le. A többieknek van egy rossz hírem: az enum nem típusannotáció, hanem kódgenerátor. Az egyetlen konstrukció a TypeScript magjában, ami fordításkor nem eltűnik, hanem JavaScriptet hagy maga után. És 2026-ra ott tartunk, hogy a TypeScript csapata külön fordítókapcsolót ad ki, amivel ki lehet tiltani a saját feature-ét a kódbázisból. Ennél őszintébb termékértékelést nehéz elképzelni.

Amit a fordító hátrahagy
Kezdjük a kvíz feloldásával. Ez a négy tag:
enum Irany {
Fel,
Le,
Balra,
Jobbra,
}
console.log(Object.keys(Irany).length); // 8
fordítás után így néz ki (a cikkhez a júliusban megjelent, Go-ban újraírt TypeScript 7.0-t használtam, de ezt az emitet évtizede változatlanul kapjuk):
var Irany;
(function (Irany) {
Irany[Irany["Fel"] = 0] = "Fel";
Irany[Irany["Le"] = 1] = "Le";
Irany[Irany["Balra"] = 2] = "Balra";
Irany[Irany["Jobbra"] = 3] = "Jobbra";
})(Irany || (Irany = {}));
A numerikus enum ugyanis reverse mappinget is kap: Irany.Fel értéke 0, de Irany[0] értéke "Fel". Ugyanabban az objektumban él a kettő, ezért lesz négy tagból nyolc kulcs. String enumnál viszont nincs reverse mapping – ott tényleg annyi kulcs van, ahány tag. Vagyis az enum kulcsszó alatt valójában két, futásidőben eltérően viselkedő feature lakik, és ezt az Object.keys-en kívül minden for...in ciklus és minden naiv szerializálás is megérzi.
A típusoldal sem makulátlan
Jó, a runtime csúnya – de legalább a típusellenőrzés vasszigorú, nem? Nos. 2023 márciusáig, a TypeScript 5.0 megjelenéséig ez szó nélkül lefordult:
enum SomeEvenDigit { Zero = 0, Two = 2, Four = 4 }
let m: SomeEvenDigit = 1; // TS 4.9: rendben. TS 5.0 óta: error TS2322
Kipróbáltam a 4.9.5-tel: tényleg lefordul, hibátlanul. Éveken át lehetett numerikus enumnak tetszőleges számot értékül adni, és a fordító bólintott rá. Az 5.0 ezt befoltozta, de egy lyuk máig megmaradt, a legfrissebb fordítóval, strict módban is:
const nev: string = Irany[999]; // típusa: string, hiba nincs
console.log(nev); // futásidőben: undefined
A reverse lookup típusa feltétel nélkül string, akkor is, ha a keresett szám nem tagja az enumnak. Egy undefined, amiről a típusrendszer esküszik, hogy string – pont az a fajta hazugság, ami ellen a TypeScriptet bevezettük.
A const enum: kevesebb runtime, több csapda
Erre a szokásos válasz a const enum, ami nem generál objektumot, hanem beinline-olja az értékeket. Ez hangzik jól, amíg meg nem nézzük, mit jelent az inline pontosan. Ezt írtam:
// lib/statusz.ts
export const enum Statusz { Aktiv = 1, Torolt = 2 }
// app/main.ts
import { Statusz } from "../lib/statusz.js";
console.log(Statusz.Aktiv);
És ezt kaptam a fordítótól:
console.log(1 /* Statusz.Aktiv */);
Az import eltűnt, a hivatkozásból szám lett. Egy alkalmazáson belül ez működik. De ha a const enum egy publikált csomagból jön, akkor a fordításkor látott A verzió értékei égnek bele a kódodba, miközben futásidőben már a B verzió is lehet telepítve – más értékekkel. A TypeScript kézikönyv külön szakaszban figyelmeztet erre, és arra is, hogy ezek a hibák azért különösen alattomosak, mert a tesztek jellemzően ugyanazokkal a függőségverziókkal futnak, mint a build – vagyis pont a CI nem fogja megfogni. Amikor a nyelv saját dokumentációja „pitfalls” címszó alatt tárgyalja a feature-t, az általában megér egy gondolatot.
Az ökoszisztéma közben döntött
A Node.js type strippingje – ami a 22.18 óta alapértelmezés – a típusokat whitespace-re cseréli, és kész. Az enumot nem tudja kicserélni, mert az nem típus, hanem kód, úgyhogy célzott hibával elhasal:
$ node iranyok.ts
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]:
TypeScript enum is not supported in strip-only mode
A Node 26 ráadásul eltávolította azt a kísérleti flaget is, ami az enumokat még lefordította – natívan soha nem fognak futni. A TypeScript oldaláról pedig az 5.8 óta létezik az --erasableSyntaxOnly kapcsoló, amivel a fordító már írás közben rászól mindenre, ami nem törölhető szintaxis: enumra, futásidejű namespace-re, parameter property-re. Maga a Node dokumentáció is ezt ajánlja bekapcsolni. Két projekt, két irányból, ugyanaz az üzenet: a TypeScript törölhető része a jövő, az enum pedig nem tartozik bele.
Amiben az enum tényleg jobb
A fair playhez hozzátartozik, hogy egy dolgot az enum tud, amit a típusrendszer más eszköze alapból nem: a string enum nominális. Ez meglepett:
enum LogLevel { Info = "info", Warn = "warn" }
const a: LogLevel = LogLevel.Info; // OK
const b: LogLevel = "info"; // error TS2322!
Hiába egyezik a literál értéke, csak maga az enum-tag adható át. Egy strukturális típusrendszerben ez ritka kincs: ingyen kapunk olyan azonosítókat, amiket nem lehet nyers stringgel hamisítani. A numerikus enum viszont – csak hogy konzisztens legyen a kép – nem nominális: az Irany típusú változónak a 2 literál ma is gond nélkül értékül adható. Ugyanaz a kulcsszó, harmadik féle viselkedés.
Az alternatíva, ami tényleg csak típus
Amit a legtöbb helyen enum helyett érdemes írni, azt maga a kézikönyv is ajánlja „Objects vs Enums” címen:
const Irany = {
Fel: 0,
Le: 1,
Balra: 2,
Jobbra: 3,
} as const;
type Irany = (typeof Irany)[keyof typeof Irany]; // 0 | 1 | 2 | 3
function lepj(i: Irany) { /* ... */ }
lepj(Irany.Balra);
Az érték és a típus ugyanazt a nevet viseli, a hívási oldal megszólalásig ugyanúgy néz ki, mint enummal. Futásidőben viszont ez egy sima objektum: négy kulcs, nulla generált kód, reverse mapping nélkül. Lefordul --erasableSyntaxOnly-val, és a fenti fájlt a Node 22 közvetlenül, build lépés nélkül lefuttatta nekem. A trade-off: a hibaüzenetekben Irany helyett néha 0 | 1 | 2 | 3 fog szerepelni, ami hosszabb unionoknál kifejezetten ronda tud lenni, és a nominalitásról is lemondunk – ha az kell, marad a string enum vagy egy branded type.
Mit csinálnék éles kódban
A const enumot publikált csomagban semmilyen körülmények között nem használnám – a verzió-skew olyan hiba, amit a CI definíció szerint nem lát. A numerikus enumot új kódban szintén nem: a reverse mapping meglepetései többe kerülnek, mint amennyit a szintaxis kényelme ér. A string enum a védhető kivétel – nincs reverse mappingje, és a nominalitása valódi, máshol drágán megszerezhető garancia. Meglévő kódbázisban eszem ágában sem lenne enumokat vadászni; ez pont az a refaktor, aminek a kockázata nagyobb, mint a haszna. De új projektet ma már erasableSyntaxOnly-val indítanék, as const objektumokkal – és akkor a kérdés fel sem merül. Az enum a TypeScript legkorábbi éveinek öröksége, abból az időből, amikor a nyelv még nyelvnek képzelte magát, nem típusrétegnek. A JavaScript azóta sem vette át, a tooling kinőtte, és a fordító már kapcsolót ad hozzá, hogy udvariasan nemet mondhassunk rá.
Források
- TypeScript Handbook: Enums – const enum pitfalls, Objects vs Enums
- TypeScript 5.0 release notes – enum-szigorítások
- TypeScript 5.8 release notes – az
--erasableSyntaxOnlyflag - Node.js dokumentáció: Modules: TypeScript
- Announcing TypeScript 7.0 – a natív fordító