TypeScript Const Enums
A const enum is a special variant of a TypeScript enum that exists only while the compiler is checking your code. Ordinary enums compile down to a real JavaScript object you can inspect at runtime, but a const enum leaves no object behind at all — every place you reference one of its members gets replaced directly with that member’s literal value. The result is an enum that reads like a normal enum in your source but costs nothing at runtime: no object allocation, no property lookup, no extra bytes in the bundle. That trade-off comes with rules and gotchas you need to understand before reaching for it.
Overview / How it works
To understand const enum, it helps to remember what a normal enum compiles to. Given enum Direction { Up, Down }, TypeScript emits a JavaScript object at runtime that maps Direction.Up to 0 and, for numeric enums, also maps 0 back to the string \"Up\" (a reverse mapping). Every time your code writes Direction.Up, the compiled JavaScript does an actual property lookup on that object.
Add the const keyword — const enum Direction { Up, Down } — and the story changes completely. TypeScript computes the value of every member during type-checking, and then, wherever your code uses Direction.Up, it substitutes the literal value 0 directly into the emitted JavaScript. No Direction object is generated at all (by default). This is a compiler feature, not a runtime feature: it’s a form of type erasure taken further than usual. Normally TypeScript only erases type information at compile time; with const enums it also erases the declaration itself, leaving only inlined values behind.
Because there is no runtime object to fall back on, the compiler must be able to fully resolve every member’s value while it is still looking at your source — it cannot defer that work to runtime. This is why const enum members must be constant expressions: numeric or string literals, or simple arithmetic/references built from other constant enum members. Anything the compiler can’t compute up front (a function call, a variable, a dynamic lookup) is rejected.
There’s also a compiler flag, preserveConstEnums, that tells TypeScript to still inline usages but also emit the backing JavaScript object, which is useful if some other tool or a debugger needs to inspect the enum’s values at runtime. And there’s an important interaction with isolatedModules (used by Babel, ts-jest, esbuild, and swc): those tools transpile one file at a time without full type information from other files, so they cannot inline a const enum that is imported from a different file, and TypeScript will error rather than silently producing wrong output.
Syntax
const enum EnumName {
MemberA,
MemberB = 10,
MemberC = MemberA + 1,
}
const— marks the enum for full inlining; no runtime object is emitted by default.enum EnumName— the usual enum declaration, naming the type.MemberA— a member with no initializer auto-numbers from0(or continues from the previous numeric member).MemberB = 10— an explicit constant expression (a literal).MemberC = MemberA + 1— a constant expression built from another member of the same const enum, which is allowed because the compiler can resolve it entirely at compile time.
Examples
Example 1: A basic numeric const enum
const enum Direction {
Up,
Down,
Left,
Right,
}
function move(direction: Direction): string {
switch (direction) {
case Direction.Up:
return \"Moving up\";
case Direction.Down:
return \"Moving down\";
case Direction.Left:
return \"Moving left\";
case Direction.Right:
return \"Moving right\";
}
}
console.log(move(Direction.Up));
console.log(move(Direction.Right));
Output:
Moving up
Moving right
Every reference to Direction.Up and Direction.Right is replaced with the literal numbers 0 and 3 in the compiled JavaScript. The Direction identifier itself disappears; only the numbers remain.
Example 2: A string const enum for configuration
const enum HttpMethod {
Get = \"GET\",
Post = \"POST\",
Put = \"PUT\",
Delete = \"DELETE\",
}
interface RequestConfig {
method: HttpMethod;
url: string;
}
function describeRequest(config: RequestConfig): string {
return `${config.method} ${config.url}`;
}
const req: RequestConfig = {
method: HttpMethod.Post,
url: \"/api/users\",
};
console.log(describeRequest(req));
Output:
POST /api/users
String const enums behave the same way: HttpMethod.Post is inlined as the literal string \"POST\". You still get autocomplete and type-checking on method: HttpMethod while writing the code, but the compiled output contains only plain strings.
Example 3: A realistic log-filtering example
const enum LogLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
}
function shouldLog(current: LogLevel, threshold: LogLevel): boolean {
return current >= threshold;
}
const messages: Array<{ level: LogLevel; text: string }> = [
{ level: LogLevel.Debug, text: \"Debug message\" },
{ level: LogLevel.Info, text: \"Info message\" },
{ level: LogLevel.Error, text: \"Error message\" },
];
const threshold = LogLevel.Info;
for (const msg of messages) {
if (shouldLog(msg.level, threshold)) {
console.log(msg.text);
}
}
Output:
Info message
Error message
This is the kind of place const enums shine: LogLevel is used purely as internal, self-documenting constants for comparisons. Nothing outside this module needs to serialize or reflect on the enum, so paying zero runtime cost for it is a clear win.
Under the hood: what the compiler actually does
- While type-checking, TypeScript computes a concrete value for every member of the const enum, in declaration order, resolving any expressions like
MemberA + 1immediately. - At every point in your code where a const enum member is referenced, the compiler looks up its precomputed value and substitutes the literal directly into the emitted JavaScript, typically alongside a comment naming the original member for readability.
- No object literal for the enum is emitted in the compiled output — unless you pass the
preserveConstEnumscompiler option, which keeps the object around (for tooling or reflection) while still inlining every usage. - Because no object exists at runtime, features that depend on one —
Object.keys(Direction), afor...inloop, or reverse lookup likeDirection[0]— are unavailable or explicitly disallowed by the type checker.
Here’s the before-and-after in miniature. This TypeScript:
const enum Status {
Active,
Inactive,
}
let s = Status.Active;
console.log(s);
Output:
0
compiles to roughly this JavaScript, with the enum’s existence erased entirely and only the literal value remaining:
let s = 0; /* Status.Active */
console.log(s);
Output:
0
This is type erasure taken to its logical conclusion: not just the type annotations vanish, but the enum construct itself is gone, leaving plain numbers or strings.
Common Mistakes
Mistake 1: Giving a member a non-constant initializer
const enum Config {
Timeout = Math.floor(1000 / 2),
}
Because a const enum has no runtime object to compute values lazily, every member must be resolvable at compile time. TypeScript rejects this with an error along the lines of \”In const enum declarations, member initializer must be constant expression\” (TS2474), since Math.floor(...) is a function call the type checker won’t evaluate for you.
Fix it by using a literal value instead:
const enum Config {
Timeout = 500,
}
console.log(Config.Timeout);
Output:
500
Mistake 2: Expecting reverse mapping to work
const enum Color {
Red,
Green,
Blue,
}
const name = Color[Color.Red];
console.log(name);
With a regular numeric enum, Color[0] would give you back the string \"Red\" because the compiler emits a reverse-mapping object. A const enum has no such object, so TypeScript flags this with \”A const enum member can only be accessed using a string literal\” (TS2476) — there is nothing at runtime to index into with a number.
If you only need to read a member by its name, index with the literal member name itself, which the compiler can resolve at compile time:
const enum Color {
Red,
Green,
Blue,
}
const name = Color[\"Red\"];
console.log(name);
Output:
0
If you genuinely need to convert a numeric value back into its member name at runtime (for logging, serialization, etc.), that’s a sign you need a regular enum, not a const enum.
Best Practices
- Reach for
const enumwhen the enum is purely an internal implementation detail used for comparisons and readability, and you never need to enumerate, serialize, or reverse-look-up its members at runtime. - Avoid
const enumin the public API of a library shipped as compiled.d.tsfiles, especially if consumers might use Babel, esbuild, swc, orts-jestwithisolatedModules— those tools transpile files independently and cannot inline a const enum imported across file boundaries, which causes a hard compiler error for consumers. - If you need the performance benefit of inlining but also want the enum object available for debugging or reflection, enable the
preserveConstEnumscompiler option. - Don’t rely on reverse mapping (
Color[0]) with const enums — it isn’t available. Use a regularenumif you need it. - Keep member initializers as plain literals or simple arithmetic on other members of the same enum; anything requiring runtime computation won’t compile.
- For simple, string-only sets of values with no need for a named type namespace, consider a union of string literals (
type Method = \"GET\" | \"POST\") as a lighter-weight alternative to a const enum — it also inlines to nothing but skips enum-specific rules entirely.
Practice Exercises
- Define a numeric
const enum Permissionwith membersNone,Read,Write, andAdmin(in increasing order), then write a functioncanAccess(userLevel: Permission, required: Permission): booleanthat returns whether the user’s level meets or exceeds the requirement. Test it against a few combinations. - Rewrite the
Configmistake example soTimeoutis derived from another member of the same const enum using addition (for example, aBaseTimeoutmember plus an offset) instead of a hardcoded literal, and confirm it still compiles. - Define a string
const enum CardSuitwithHearts,Diamonds,Clubs, andSpades, then write a functionsuitColor(suit: CardSuit): stringthat returns\"Red\"for hearts/diamonds and\"Black\"for clubs/spades using a switch statement.
Summary
- A
const enumis fully inlined at compile time; by default no JavaScript object is emitted for it at all. - Every usage is replaced with the member’s literal value, giving zero runtime cost compared to a regular enum’s object and property lookups.
- Member initializers must be constant expressions the compiler can resolve while type-checking — no function calls or dynamic values.
- Reverse mapping (looking up a member name from its numeric value) is not available for const enums.
isolatedModulesenvironments (Babel, esbuild, swc, ts-jest) cannot inline const enums imported from other files, so avoid them in cross-file public APIs.- Use
preserveConstEnumsif you need both inlining and a real runtime object for tooling. - Prefer const enums for internal-only constants; prefer regular enums or string literal unions when you need runtime enumeration, serialization, or reverse lookup.
