TypeScript Enum: Numeric, String, const and as const

Numeric, string and const enums in TypeScript, reverse mapping and string-to-enum conversion, plus why Node.js type stripping and erasableSyntaxOnly reject enums and what to use instead.

angular-typescript

TypeScript enums (short for enumerations) give names to a fixed group of constants, such as the sizes Small, Medium and Large. Each member has a value, either a number or a string, and the enum name can be used 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: the compiler turns it into a JavaScript object.

That last point has become important. Node.js 22 runs .ts files directly by stripping the types, and it rejects enums because they cannot be stripped. TypeScript 5.8 added the erasableSyntaxOnly option, which reports the same problem at compile time. This article covers numeric, string, heterogeneous and const enums, reverse mapping, iterating over an enum, converting a string to an enum, and the as const object that many projects now use instead. Every example was compiled with TypeScript 7.0.2 and run on Node.js 22.22.

Numeric and string enums, a const enum, and the erasable alternative look like this side by side.

// 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"

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. When we assign a number to a member, the counting continues from there.

// 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. Any value already stored in a database then 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. 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.

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 exactly that string. Assigning fruit = “apple” to a variable of type Fruit fails:

error TS2322: Type '"apple"' is not assignable to type 'Fruit'.

This rule forces code to go through the enum, which some teams like. It also means that a string read from JSON, a form or a URL must be converted to the enum first, which section 5 shows.

3. Heterogeneous Enums

An enum can mix number and string members. The TypeScript Handbook advises against it, and there is rarely a reason to do it, because each member then 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 quick-reference snippet into this code:

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]. It works only for numeric enums. The snippet uses the Size and Fruit enums from the quick-reference snippet. The extra entries also appear when we loop over a numeric enum, which is a common source of bugs: Object.keys() returns the values as strings in addition to the names.

// 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. 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 every use of a member is replaced by its value. 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. Tools that compile one file at a time, such as esbuild, SWC, Babel and Node.js type stripping, cannot do that across files. The TypeScript Handbook describes these problems in its section on 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 directly with node file.ts. It does this by type stripping: 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. The Node.js TypeScript documentation lists three constructs as unsupported:

  • 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. 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. 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. 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. The limit applies to code that runs directly through type stripping, and to teams that want to keep that option open.

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. 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 then reads Size.Large for the value and Size for the type, as with an enum. The type expression works in two steps: keyof typeof Size gives the keys “Small” | “Medium” | “Large”, and indexing the object type with those keys gives the values. The literal types article explains as const and widening in detail.

The project’s strip-demo/as-const.ts file contains this pattern and runs with plain node, printing “medium”.

FeatureString enumas const object + union
Runs with Node.js type strippingNoYes
Allowed with erasableSyntaxOnlyNoYes
Accepts a plain string such as “small”No, only Fruit.AppleYes, when it is one of the values
Values at runtimeObject.values(Fruit)Object.values(Size)
Member access syntaxFruit.AppleSize.Small
Extra JavaScript emittedAn IIFE that builds the objectThe 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. 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; 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, string enums are readable at runtime but reject plain strings, and const enums are inlined but do 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, 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.

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.