TypeScript enums (short for enumerations) give names to a fixed group of constants, such as Size.Small or Size.Large. Each member has a value, either a number or a string, and we can use the enum name as a type, so a parameter of type Size accepts only the values of Size. Unlike almost every other TypeScript feature, an enum is not removed during compilation, because the compiler turns it into a JavaScript object.
We use an enum when a value must be one of a known set, such as the cup size of a coffee order or the status of a payment.
The following example shows numeric and string enums, a const enum and the erasable as const alternative side by side. The snippets compile with TypeScript 7.0.2 and run on Node.js 22.22.
// 1. Numeric enum: members get 0, 1, 2
enum Size {
Small,
Medium,
Large,
}
const m = Size.Medium; // m = 1
const label = Size[1]; // label = "Medium", reverse mapping
// 2. String enum
enum Fruit {
Apple = "apple",
Banana = "banana",
}
const f = Fruit.Banana; // f = "banana"
// 3. const enum: value inlined at compile time
const enum Dice {
One = 1,
Two = 2,
}
const d = Dice.Two; // d = 2
// 4. Enum as a parameter type
function price(size: Size): number {
return size === Size.Large ? 9 : 5;
}
const p = price(Size.Large); // p = 9
// 5. Erasable alternative: as const object + union type
const Color = { Red: "red", Green: "green" } as const;
type Color = (typeof Color)[keyof typeof Color]; // "red" | "green"
const c: Color = Color.Green; // c = "green"
Notice that the numeric enum Size maps the value 1 back to the name “Medium”, whereas the as const object Color is a plain object with a union type of the same name.
The fact that an enum compiles to an object has become important, because Node.js 22 runs .ts files by stripping the types, and it rejects enums since they cannot be stripped. TypeScript 5.8 added the erasableSyntaxOnly option, which reports the same problem at compile time. We cover numeric, string, heterogeneous and const enums first, followed by reverse mapping, iteration and string conversion, and end with the as const object that many projects use instead.
1. Numeric Enums
A numeric enum is the default kind. When we do not assign values, the first member gets 0 and each following member gets the previous value plus 1, and when we assign a number to a member, the counting continues from there. A numeric enum fits values that are numbers outside our code too, such as the priority levels a ticket system stores as 10 and 20.
// 1. Default values start at 0
enum Size {
Small, // 0
Medium, // 1
Large, // 2
}
// 2. Custom start, then +1
enum Dice {
One = 1, // 1
Two, // 2
Three, // 3
}
// 3. Any explicit numbers
enum Level {
Low = 10,
High = 20,
}
// 4. Enum as a type
let size: Size = Size.Medium;
size = Size.Large;
const three = Dice.Three; // three = 3
const high = Level.High; // high = 20
A numeric enum type accepts its members and the numbers they stand for. Since TypeScript 5.0, a number literal that matches no member is a compile error, so size = 5 is rejected while size = 1 still compiles, because 1 is the value of Size.Medium.
error TS2322: Type '5' is not assignable to type 'Size'.
The values themselves are the weak point of numeric enums. A log line or a JSON response shows 2 instead of Large, and inserting a new member in the middle of the list shifts the values of all members after it, so any value already stored in a database points to the wrong member. When we use numeric enums, we assign every value explicitly.
2. String Enums
In a string enum, every member must be initialized with a string, because there is no auto-increment. String values are readable in logs, in JSON and in the debugger, which is why most TypeScript code that uses enums prefers string enums. For example, an order service that puts Status.Shipped into its JSON response sends “shipped”, which the frontend team reads without a lookup table.
enum Fruit {
Apple = "apple",
Banana = "banana",
Cherry = "cherry",
}
// 1. Read a member
const fruit = Fruit.Apple; // fruit = "apple"
// 2. Compare with ===
const isApple = fruit === Fruit.Apple; // isApple = true
// 3. Enum value in a string
const text = "I like " + Fruit.Cherry; // text = "I like cherry"
A string enum behaves differently from a union of string literals in one way that confuses many developers. The plain string “apple” is not assignable to the type Fruit, even though Fruit.Apple holds that same string. Assigning fruit = “apple” to a variable of type Fruit fails.
error TS2322: Type '"apple"' is not assignable to type 'Fruit'.
The rule that only members fit forces code to go through the enum, which some teams like. It also means that a string read from JSON or a URL must be converted to the enum first, as shown in section 5.
3. Heterogeneous Enums
An enum can mix number and string members, but the TypeScript Handbook advises against it, and there is rarely a reason to do it, because each member behaves differently.
enum Answer {
No = 0,
Yes = "yes",
}
const no = Answer.No; // no = 0
const yes = Answer.Yes; // yes = "yes"
4. What an Enum Compiles To
Reading the compiled JavaScript explains most enum behavior. The TypeScript 7.0.2 compiler turns Size and Fruit from the intro example into two functions that run once and fill a plain object each.
var Size;
(function (Size) {
Size[Size["Small"] = 0] = "Small";
Size[Size["Medium"] = 1] = "Medium";
Size[Size["Large"] = 2] = "Large";
})(Size || (Size = {}));
var Fruit;
(function (Fruit) {
Fruit["Apple"] = "apple";
Fruit["Banana"] = "banana";
})(Fruit || (Fruit = {}));
The line Size[Size[“Small”] = 0] = “Small” does two assignments. The inner one sets Size.Small to 0, and the assignment expression returns 0, so the outer one sets Size[0] to “Small”. The two kinds of enum objects therefore hold different entries.
- A numeric enum object maps names to values and values back to names.
- A string enum object maps names to values only.
4.1. Reverse Mapping and Iterating Over an Enum
Reverse mapping means reading a member name from its value, as in Size[2], and it works only for numeric enums. The Size and Fruit enums in the snippet are the ones from the intro example. The extra entries also appear when we loop over a numeric enum, which is a common source of bugs, because Object.keys() returns the values as strings in addition to the names. For example, a settings page that builds a dropdown from Object.keys(Size) shows six options instead of the three sizes.
// 1. Reverse mapping: value to name (numeric enums only)
const name = Size[2]; // name = "Large"
// 2. A numeric enum object holds both directions
const keys = Object.keys(Size); // keys = ["0", "1", "2", "Small", "Medium", "Large"]
const names = Object.keys(Size).filter((k) => isNaN(Number(k)));
// names = ["Small", "Medium", "Large"]
// 3. A string enum holds names and values once
const fruitNames = Object.keys(Fruit); // fruitNames = ["Apple", "Banana"]
const fruitValues = Object.values(Fruit); // fruitValues = ["apple", "banana"]
// 4. Names as a union type
type FruitName = keyof typeof Fruit; // "Apple" | "Banana"
const key: FruitName = "Banana";
const banana = Fruit[key]; // banana = "banana"
The filter in the second example keeps only the keys that are not numbers, which are the member names. In the last example, typeof Fruit is the type of the enum object, and keyof turns its property names into the union “Apple” | “Banana”.
5. Converting a String to an Enum
Values from outside the program arrive as plain strings, such as the fruit query parameter of a shop page. We convert them by value (“banana” to Fruit.Banana) or by member name (“Apple” to Fruit.Apple), and both functions return undefined for unknown input so the caller has to handle it.
// 1. By value: "banana" -> Fruit.Banana
function toFruit(value: string): Fruit | undefined {
return Object.values(Fruit).find((f) => f === value);
}
const f1 = toFruit("banana"); // f1 = Fruit.Banana ("banana")
const f2 = toFruit("mango"); // f2 = undefined
// 2. By name: "Apple" -> Fruit.Apple
function fromName(name: string): Fruit | undefined {
return name in Fruit ? Fruit[name as keyof typeof Fruit] : undefined;
}
const f3 = fromName("Apple"); // f3 = Fruit.Apple ("apple")
Object.values(Fruit) has the type Fruit[], so find() returns Fruit | undefined without a cast. For a numeric enum, Object.values() also returns the member names because of reverse mapping, so we would filter by typeof v === “number” first. A direct lookup such as Fruit[“apple”] does not work, because “apple” is a value and not a member name; the compiler reports Property ‘apple’ does not exist on type ‘typeof Fruit’. Did you mean ‘Apple’?
6. const Enums
A const enum is removed during compilation, and the compiler replaces every use of a member with its value, so no enum object exists at runtime, which saves a little code. The compiled form of const roll = Dice.Three is const roll = 3 /* Dice.Three */.
const enum Dice {
One = 1,
Two = 2,
Three = 3,
}
const roll = Dice.Three; // compiled to: const roll = 3
const sum = Dice.One + Dice.Two; // sum = 3
Inlining needs the compiler to read the enum declaration while it compiles each file that uses it, which tools that compile one file at a time, such as esbuild, SWC, Babel and Node.js type stripping, cannot do across files. The TypeScript Handbook calls these problems const enum pitfalls, and many projects avoid const enums for that reason. Because no object exists, reverse mapping and Object.values() are also unavailable.
7. Enums, Node.js Type Stripping and erasableSyntaxOnly
Since version 22.18, Node.js runs TypeScript files with node file.ts by type stripping, which means it replaces type annotations with spaces and does not type-check or transform anything. Syntax that produces JavaScript code, such as an enum, cannot be handled that way. For example, a team that runs its build scripts with node scripts/release.ts gets an error as soon as one script declares an enum. Node.js type stripping does not support these constructs.
- Enums
- Namespaces with runtime code
- Constructor parameter properties
enum Size {
Small,
Medium,
Large,
}
console.log(Size.Medium);
Running this file with Node.js 22.22.0 fails before the first line executes, and the same error appears with the older –experimental-strip-types flag.
file:///.../strip-demo/enum.ts:1
> enum Size {
Small,
Medium,
Large,
> }
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
The –experimental-transform-types flag makes Node.js compile the enum and print 1, but it prints ExperimentalWarning: Transform Types is an experimental feature and might change at any time, so code that is meant to run with plain node should not depend on it.
To find such code before running it, TypeScript 5.8 added the erasableSyntaxOnly compiler option. With it enabled, tsc reports every construct that type stripping cannot handle, including regular enums and const enums, and only declare enum, which describes an enum that exists elsewhere and produces no code, is allowed.
{
"compilerOptions": {
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true
}
}
strip-demo/enum.ts(1,6): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
Projects that compile with tsc or a bundler before running, as the example project for this article does, can still use enums, because the limit applies only to code that runs through type stripping, and to teams that want to keep type stripping open as an option.
8. The as const Object as an Enum Replacement
An object with as const plus a union type of its values gives the same developer experience as a string enum and is fully erasable. The object exists at runtime, so we can list its values, whereas the type with the same name is the union of those values, and it disappears during compilation like any other type.
// 1. Object with the values, frozen by as const
const Size = {
Small: "small",
Medium: "medium",
Large: "large",
} as const;
// 2. Type with the same name: union of the values
type Size = (typeof Size)[keyof typeof Size]; // "small" | "medium" | "large"
// 3. Use it like an enum
function price(size: Size): number {
return size === Size.Large ? 9 : 5;
}
const p1 = price(Size.Large); // p1 = 9
const p2 = price("small"); // p2 = 5, plain string accepted
// 4. List and check values at runtime
const all = Object.values(Size); // all = ["small", "medium", "large"]
const valid = (all as string[]).includes("medium"); // valid = true
TypeScript allows a value and a type to share the name Size, because values and types live in separate namespaces. Code reads Size.Large for the value and Size for the type, as with an enum. The type expression works in two steps. First, keyof typeof Size gives the keys “Small” | “Medium” | “Large”, and indexing the object type with those keys gives the values. The as const assertion keeps each value as a literal type instead of widening it to string.
The project’s strip-demo/as-const.ts file contains this pattern and runs with plain node, printing “medium”. A string enum and an as const object differ in the points that matter for a project that may move to type stripping.
| Feature | String enum | as const object + union |
|---|---|---|
| Runs with Node.js type stripping | No | Yes |
| Allowed with erasableSyntaxOnly | No | Yes |
| Accepts a plain string such as “small” | No, only Fruit.Apple | Yes, when it is one of the values |
| Values at runtime | Object.values(Fruit) | Object.values(Size) |
| Member access syntax | Fruit.Apple | Size.Small |
| Extra JavaScript emitted | An IIFE that builds the object | The object literal only |
For new code, we use the as const object or a plain string literal union. Existing enums keep working with tsc, so there is no need to replace them unless the project moves to type stripping. Java developers often expect an enum to be a class with fields and methods, but a TypeScript enum is only a set of named constants, so the as const object loses nothing in comparison.
9. Running the Enum Examples
The enums folder on GitHub has one file per section in src/, the two type-stripping demos in strip-demo/, and a separate tsconfig.erasable.json with erasableSyntaxOnly turned on. It uses TypeScript 7.0.2 and Node.js 22.18 or newer for the type-stripping scripts.
npm install
npm start # compile src/ with tsc and run it
npm run strip:enum # fails: enum is not supported in strip-only mode
npm run strip:as-const # prints "medium"
npm run check:erasable # fails with TS1294 for strip-demo/enum.ts
The two failing scripts are expected to fail, because they reproduce the errors from section 7.
10. Conclusion
An enum names a fixed set of numbers or strings. Numeric enums count from 0 and support reverse mapping, whereas string enums are readable at runtime but reject plain strings. A const enum is inlined, but it does not work well with single-file compilers.
Because enums generate JavaScript, Node.js type stripping and the erasableSyntaxOnly option reject them. For new code, an as const object with a matching union type gives the same Size.Large syntax. It also accepts plain strings that match and runs anywhere TypeScript runs.
11. References
The TypeScript Handbook and the 5.8 release notes describe enums and erasableSyntaxOnly, and the Node.js documentation describes what type stripping supports.
- TypeScript Handbook: Enums
- TypeScript 5.8 release notes: the erasableSyntaxOnly option
- TSConfig reference: erasableSyntaxOnly
- Node.js 22 documentation: TypeScript and type stripping
- MDN: Object.values()
Happy Learning !!