Skip to content(if available)orjump to list(if available)

TypeScript enums: use cases and alternatives

skrebbel

One thing I'm missing in the comments here is that enums are a very early TypeScript feature. They were in there nearly from the start, when the project was still trying to find clarity on its goals and principles.

Since then:

- TypeScript added string literals and unions, eg `type Status = "Active" | "Inactive"`

- TypeScript added `as const`, eg `const Status = { Active: 0, Inactive: 1 } as const`

- TypeScript adopted a stance that features should only generate runtime code when it's on a standards track

Enums made some sense back when TS didn't have any of these. They don't really make a lot of sense now. I think they're effectively deprecated, to the point that I wonder why they don't document them as deprecated.

mistercow

I think they also haven't gotten very much attention in the last few years as new features have been added. Nine times out of ten, if I hit a weird case where TS doesn't understand some type that it really seems like it should understand, it involves an enum. And if I rewrite the enum as a union type and update the other code that uses it, my issue goes away.

I agree they should just formally deprecate it.

gherkinnn

And I bet 6 out of those 9 times it is because enums are nominally typed when the rest of TS is structurally typed.

mistercow

I think that's part of the underlying issue in every case, but then there sometimes seems to be some kind of bug where TS won't agree that the value actually has that nominal type, despite it originating from the enum itself. I usually then can't reproduce these issues with more minimal examples.

null

[deleted]

n144q

They still make sense in terms of clarity, readability and reusability. I use enum every time there are more than 2 entries -- literal types and "as const" are just ugly in comparison.

Not to mention that you can add documentation to each of the entries.

WorldMaker

    type SomeProcessState =
      // when waiting for user interaction
      | 'idle'
      // busy processing
      | 'busy'
      // finished processing
      | 'success'

    const OtherProcessStates = Object.freeze({
      /**
       Waiting for user interaction
      */
      Idle: 0,
      /**
       Busy processing
      */
      Busy: 1,
      /**
       Finished processing
      */
      Success: 2,
    } as const)
    type OtherProcessState = typeof OtherProcessStates[keyof typeof OtherProcessStates]
The second form those are even working JSDOC comments.

bryanrasmussen

I'm not often given to aesthetic pronouncements regarding code, but I have to agree that is rather ugly looking.

n144q

Of course they work, I use this in JavaScript all the time. But why would I do that when I have real enum in TypeScript?

motorest

You just posted a justification to use enums.

mistercow

> Not to mention that you can add documentation to each of the entries.

You can do that with either other solution.

leidenfrost

I wonder if there's a guide of recommendations about typescript now deprecated features, and its modern equivalents.

WorldMaker

Given Typescript has preferred opt-in strictness flags for its recommendations, the two big places that seem to be Typescript's best documentation of "deprecated" features seems to be:

1) verbatimModuleSyntax -- https://www.typescriptlang.org/tsconfig/#verbatimModuleSynta...

2) isolatedModules -- https://www.typescriptlang.org/tsconfig/#isolatedModules

Between the two of those flags, enums and namespaces and a few other things get disabled.

The flag documentation doesn't explain what the modern equivalents are, though. I suppose that's currently left to blog posts like the one linked here.

inbx0

isolatedModules doesn't disable enums in general, only exported const enums (which are arguably the most useful form of enums).

msoad

Yeah they should deprecate namespaces and enums in the next major version... oh wait...

TypeScript versioning is literally a joke

Dylan16807

What are you implying with "oh wait"? They have major versions every once in a while, and breaking changes. They could do that.

cap11235

It's js devs, what do you expect

tgv

Changing values (after a change in an external interface), tracking use and renaming is harder in the first case. In the second case, the code can change the value at runtime.

mistercow

> Changing values (after a change in an external interface), tracking use and renaming is harder in the first case.

You can rename the elements of a string union with the typescript language server. In VS Code at least, it's just like renaming a variable, and it updates the usages which use the type.

> In the second case, the code can change the value at runtime.

You can always freeze the object if you're worried about that.

skrebbel

> Changing values (after a change in an external interface), tracking use and renaming is harder in the first case.

FWIW in VS Code I can rename a string literal (in the type definition) and it's renamed everywhere. Similarly I can use "Find All References", it just works. Pretty cool!

null

[deleted]

msoad

After almost a decade of TypeScript my recommendation is to not use TypeScript enums.

Enums is going to make your TypeScript code not work in a future where TypeScript code can be run with Node.js or in browser when typings are added to JavaScript[1]

Enums results in runtime code and in most cases you really want type enums. Use `type State = "Active" | "Inactive"` and so on instead. And if you really want an closed-ended object use `const State = { Active: 1, Inactive: 0 } as const`

All of the examples in the article can be achieved without enums. See https://www.typescriptlang.org/play/?#code/PTAEFEA8EMFsAcA2B...

[1] https://github.com/tc39/proposal-type-annotations

throwitaway1123

> Enums is going to make your TypeScript code not work in a future where TypeScript code can be run with Node.js

Apparently they're planning on adding a tsconfig option to disallow these Node-incompatible features as well [1].

Using this limited subset of TS also allows your code to compile with Bloomberg's ts-blank-space, which literally just replaces type declarations with whitespace [2].

[1] https://github.com/microsoft/TypeScript/issues/59601

[2] https://bloomberg.github.io/ts-blank-space/

WorldMaker

Those flags have already started to show up in today's typescript: verbatimModuleSyntax [1] and isolatedModules [2], for instance.

[1] https://www.typescriptlang.org/tsconfig/#verbatimModuleSynta...

[2] https://www.typescriptlang.org/tsconfig/#isolatedModules

throwitaway1123

Those definitely help, but the proposed erasableSyntaxOnly flag would disallow all features that can't be erased. So it would prevent you from using features like parameter properties, enums, namespaces, and experimental decorators.

It would essentially help you produce TypeScript that's compatible with the --experimental-strip-types flag (and ts-blank-space), rather than the --experimental-transform-types flag, which is nice because (as someone else in this thread pointed out), Node 23 enables the --experimental-strip-types flag by default: https://nodejs.org/en/blog/release/v23.6.0#unflagging---expe...

madeofpalk

> in a future where TypeScript code can be run with Node.js

FYI, this is now. Node 23.6 will just run typescript files than can have their types stripped https://nodejs.org/en/blog/release/v23.6.0#unflagging---expe....

There is a seperate --experimental-transform-types flag which'll also transform for enums, but no idea if they ever intend to make this not experimental or unflagged.

plopz

I think the biggest hurdle in getting something like that to work is how typescript handles the import syntax

WorldMaker

Most of the "drama" in recent Typescript, such as requiring file extensions, with the import syntax has been aligning with the Browser/Node requirements. If you set the output format to a recent enough ESM and the target platform to a recent enough ES standard or Node version it will be a little more annoying about file extensions, but the benefit is that it import syntax will just work in the browser or in Node.

The only other twist to import syntax is marking type-only imports with the type keyword so that those imports can be completely ignored by simple type removers like Node's. You can turn that check on today in Typescript's compile options with the verbatimModuleSyntax [1] flag, or various eslint rules.

[1] https://www.typescriptlang.org/tsconfig/#verbatimModuleSynta...

madeofpalk

you just tell typescript to stay away from import syntax, and use node-native resolution and it all just works.

its 2025 and node is finally good :)

n144q

That's a stage 1 proposal that has barely gained any traction since its release. In fact it hasn't been updated for quite a while (with "real" content changes). I wouldn't make decisions for my current code based on something that probably will never happen in the future.

https://github.com/tc39/proposal-type-annotations/commits/ma...

eyelidlessness

It’s moving slowly, but I think it’s almost inevitable. Type annotations are generally gaining or maintaining their already widespread popularity, and bringing them into the language syntax would just be an acknowledgment of that fact. I think the only thing that might hold that back is the proposal’s commitment to non-TypeScript use cases, which while magnanimous is a huge opportunity for the kinds of bike shedding that might tank a proposal like it.

runarberg

TC-39 is kind of lame at the moment. I can’t imagine they will stay this lame forever. There are some reasonable voices inside TC-39, so even thought currently the lame voices at the committee are more powerful, that could change at any moment.

FjordWarden

I understand, but what if I want to use the enums the way they are used in C, as a label for a number, probably as a way to encode some type or another. Sum types of literal numbers are not very practical here because the labels should be part of the API.

mistercow

What in your view is the downside to doing this?

    export const MyEnumMapping = {
      active: 0,
      inactive: 1
    } as const

    export type MyEnum = typeof MyEnumMapping[keyof typeof MyEnumMapping];
So you have the names exposed, but the underlying type is the number.

eeue56

I would do this instead:

  type MyEnum = {
    active: 0;
    inactive: 1;
  }

  const MyEnum: MyEnum = {
    active: 0, 
    inactive: 1,
  }

  const showAge = MyEnum.active;
  const showPets = MyEnum.inactive;

It's slightly more duplication, but a lot more readable (imo) to those unfamiliar to utility types. TypeScript also enforces keeping them in sync.

akdev1l

This is way harder to parse and understand than the enum alternative.

Personally I am definitely not skilled enough at typescript to come up with this on my own before seeing this thread so this was not even an option until now.

Kiro

Is this what you're referring to when you're talking about more elegant alternatives? Come on. You're not going to convince anyone with this.

anamexis

In that case you can just use object literals `as const`.

baq

Maybe argue for enum being added to ecmascript instead?

mistercow

But why? The feature offers almost no benefit in TS at this point over other existing features, has no function in JS other than TS compatibility, and is increasingly flaky in TS itself. Adding more complexity to JS rather than simplifying TS by deprecating this old, janky foot gun and educating devs on better alternatives seems like moving in the wrong direction.

zarzavat

TypeScript didn't invent enums. They exist because it really sucks to write out:

    const MyEnum = {
      x: 1,
      y: 2,
      z: 3,
      // etc
    }
instead of

    enum MyEnum {
      x = 1,
      y,
      z,
      // etc
    }
when you want a series of constants each with a unique value but don't particularly care what that value is.

TypeScript's enums are particularly weak compared to enums in other languages precisely because there's no JS support for enums. Modern languages have support for ADTs.

bogdan

You're correct. Nodejs can already run typescript code directly but it only does type stripping so it won't work with enums or namespaces which need additional code generated at build time.

Klaster_1

Often, I find myself in need to find all references of "Active" from your example, which doesn't work with union values. This looks like a LSP limitation. Of course, you can move assign values into consts and union these instead. But that means you are half way there to custom run-time enums, and all the way after you wrap the consts with an object in order to enumerate over values at run-time.

mistercow

> Often, I find myself in need to find all references of "Active" from your example, which doesn't work with union values.

I'm able to do that just fine in VS Code / Cursor.

I set up a union like this:

    export type TestUnion = 'foo' | 'bar' | 'baz';
Then use it in another file like this:

    const bar: TestUnion = 'bar';
    const barString: string = 'bar';
If I select 'bar' from the type and choose "Go to references", it shows me the `const bar` line, but not the `const barString` line, which is what I would expect.

homebrewer

Use `const enum Foo`, they leave no traces in the transpiled JS and provide good IDE experience.

girvo

Agreed. Its one of my major annoyances with Relay, is that it generates enums.

ivanjermakov

I use TypeScript in a way that leaves no TS traces in compiled JS. It means no enums, no namespaces, no private properties, etc.

Great list of such features: https://www.totaltypescript.com/books/total-typescript-essen...

TS has a great type system, the rest of the language is runtime overhead.

preommr

> no private properties

Private properties have been in the works for the last 7-8 years, and were officially added three years ago.

msoad

I think they are referring to `class Foo { constructor(private bar: string) }`

ivanjermakov

I was talking about useDefineForClassFields and Object.defineProperty with which I encountered performance issues.

conaclos

The suggested alternative looks overly complex to me. Moreover, it uses the `__proto__` property that is deprecated [0] and never was standardized. I could write something like this instead:

  type MyEnum = typeof MyEnum[keyof typeof MyEnum];
  const MyEnum = {
    A: 0,
    B: 1,
  } as const;
Unfortunately I found it still more verbose and less intuitive than:

  enum MyEnum {
    A = 0,
    B = 1,
  }
TypeScript enum are also more type-safe than regular union types because they are "nominally typed": values from one enum are not assignable to a variable with a distinct enum type.

This is why I'm still using TypeScript enum, even if I really dislike the generated code and the provided features (enum extensions, value bindings `MyEnum[0] == 0`).

Also, some bundlers such as ESbuil are able to inline some TypeScript enum. This makes TypeScript enum superior on this regard.

In a parallel world, I could like the latter to be a syntaxic sugar to the former. There were some discussions [1] for adopting a new syntax like:

  const MyEnum = {
    A: 0,
    A: 1,
  } as enum;
[0] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

[1] https://github.com/microsoft/TypeScript/issues/59658

moogly

Re: __proto__, it's addressed in TFA

> Note that __proto__ also exists as a getter and a setter in Object.prototype. This feature is deprecated in favor of Object.getPrototypeOf() and Object.setPrototypeOf(). However, that is different from using this name in an object literal – which is not deprecated.

conaclos

Thanks for the reply. I was not aware of this.

In this case, I could write this:

  type Activation = "Active" | "Inactive";
  const Activation = {
    __proto__: null,
    Active: "Active",
    Inactive: "Inactive",
  } as { [K in Activation]: K };
This completely hides `__proto__` and avoid using utility types like `Exclude`.

Note that it is safe because TypeScript checks that the type assertion is valid. If I mistype a value, TypeScript will complain about the assertion.

Aeolun

I don’t understand all these comments. I use TS enums like I use Java enums and I literally never have issues. What are y’all doing with these?

y-c-o-m-b

I agree; I'm in FAANG and it's even encouraged in my group. They are super easy to construct and work with and we've yet to encounter any side effects of using them despite our millions of customers world-wide.

Maybe people should become familiar with Grug: https://grugbrain.dev/

I'll take the t-rex.

> apex predator of grug is complexity. complexity bad. say again: complexity very bad. you say now: complexity very, very bad. given choice between complexity or one on one against t-rex, grug take t-rex: at least grug see t-rex

pavel_lishin

I've had issues with enums on at least three occasions, but I cannot remember what they actually were - but every time I've tried using them, I've regretted it.

sirlone

I can't remember the last time I needed enums. Usually they fall out of the code in one way or another

    // in some util.ts or helpers.ts
    export function keysOf<T extends string>(obj: { [k in T]: unknown }): readonly T[] {
       return Object.keys(obj) as unknown[] as T[];
    }

    //---

    const kOperationsFunctions = {
       add(a: number, b: number) { return a + b; },
       subtract(a: number, b: number) { return a - b; },
       multiply(a: number, b: number) { return a * b; },
       divide(a: number, b: number) { return a / b; },
    };
    const kOperations = keysOf(kOperationsFunctions);
    type Operation = (typeof kOperations)[number];
I don't want to declare an enum here. That would just be a waste of typing and an opportunity for things to get out of sync.

shortrounddev2

In typescript, some types are values. Typescript treats enums as types, but they're secretly values. Classes are types and values.

ARandumGuy

How does that affect things in real code bases? I'm with Aeolun on this. I work with Typescript enums all the time, and have never encountered any issues. Maybe the other options work slightly better. But I struggle to see how Typescript enums could cause any problems when used like I'd use enums in any other language.

s900mhz

Same, I have use enum strings for years and never had an issue.

Kiro

Yeah, people arguing against enums in this thread are doing a really bad job. I haven't seen a single compelling argument.

joshstrange

We alway use this in place of ENUMs:

    export const SMS_TYPE = {
        BULK: 'bulk',
        MARKETING: 'marketing',
        PIN: 'pin',
        SIGNUP: 'signup',
        TRANSACTION: 'transaction',
        TEST: 'test',
    } as const;
    export type SmsType = typeof SMS_TYPE[keyof typeof SMS_TYPE];

ENUMs (at least in my experience, which may be dated) had a number of drawbacks that pushed us to this format. I vaguely remember having issues parsing data from the server and/or sending ENUM values to the server but it's been a long time and I've been using this const pattern for around 5 years or so now.

ralusek

Exactly what I do, but I've found that I almost always will add a few things that come in handy.

    export const SMS_TYPE = Object.freeze({
        BULK: 'bulk',
        MARKETING: 'marketing',
        PIN: 'pin',
        SIGNUP: 'signup',
        TRANSACTION: 'transaction',
        TEST: 'test',
    } as const);

    export const SMS_TYPE_LIST = Object.freeze(Object.values(SMS_TYPE));

    export const SMS_TYPE_SET = Object.freeze(new Set(SMS_TYPE_LIST));

    export type SmsType = typeof SMS_TYPE[keyof typeof SMS_TYPE];

recursive

You can simplify the `SmsType` declaration a bit.

    export type SmsType = typeof SMS_TYPE_LIST[number];

joshstrange

Nice, I will have to watch for cases where I could make use of that!

I think I originally found the const-style on somewhere HN or SO but it has changed a bit over the years due to suggestions people have made so thank you for contributing to improvement of this pattern for me.

ralusek

I end up using the sets a lot in particular for any case where you're dealing with unvalidated data.

Type checks where you need to say, for example, SMS_TYPE_SET.has(someValue)

sirlone

why repeat the names?

    // put this in some util.ts or enum.ts file
    function makeEnum<T extends readonly string[]>(keys: T) {
        return Object.fromEntries(keys.map((x) => [x, x])) as {
            [K in (typeof keys)[number]]: K
        };
    }

    const SMS_TYPES = makeEnum(['bulk', 'marketing', 'pin', 'signup', 'transaction', 'test'] as const);
    type SMS_TYPE = keyof typeof SMS_TYPES;

bluelightning2k

I personally see TS enums as an anti-pattern.

One big reason: you can't name it interfaces.d.ts, or import as type, which has widespread implications:

Your types are now affecting your shipped bundles.

Sure that's a small bit of size - but it can actually lead to things like server side code getting shipped to the client.

Whereas if it's all .d.ts stuff you know there's no risk of chained dependencies.

I'd go so far as to say default eslint rules should disallow enums.

mistercow

I’ve also seen them behave very weirdly and inconsistently. There have been cases when I’ve had to explicitly declare that a value has an enum type, even though its type is already one of the enum’s values (and not a literal of the same value, but literally straight from the enum itself).

From what I can tell, they were an early addition from back before TS had unions, and it feels like they live in their own world within the type system. I would go further than saying you should disallow them with a linter, and say that they should be deprecated in the language. Right now they’re just a foot gun for new TS devs.

estsauver

I like how this article demystifies TypeScript enums—especially around numeric vs. string values and all the weird runtime quirks. Personally, I mostly steer clear of numeric enums because of that dual key/value mapping, which can be as confusing as Scala’s old-school Enumeration type (where numeric IDs can shift if you reorder entries). In Scala, it’s often better to use sealed traits and case objects for exhaustiveness checks and more explicit naming—kind of like TS’s union-of-literal types.

If you just need a fixed set of constants, union types with never-based exhaustiveness checks feel simpler and more “ADT–style.” That approach avoids generating the extra JS code of enums and plays nicer with certain “strip-only” TypeScript setups. In other words, if you’ve ever regretted using Enumeration in Scala because pattern matching turned messy or IDs moved around, then you’ll probably want to keep TypeScript enums at arm’s length too—or at least stick to string enums for clarity.

chpatrick

I think type-level string unions are the way to go. They're concise, efficient (the strings are interned anyway), and when you're debugging you know what the values are rather than getting mysterious integers.

pspeter3

Are all strings under a certain length interned?

rednafi

For someone who writes TS only occasionally and mostly doesn't care about the JS ecosystem, this is a great article. I picked up a few tricks. That said, normalization of warts is a common thing in JS, and people tend to just live with it rather than fix it. This feels like another example of that.

In Go, if something is discouraged (unsafe, runtime, reflection shenanigans), you immediately know why. The language is mostly free of things that exist but you shouldn’t use.

TS was a breath of fresh air when it came out. I never took Node seriously for backend work—it was always something I reluctantly touched for client-side stuff. But TS made some of JS’s warts bearable. Over time, though, it’s added so many crufts and features that these days, I shudder at the thought of reading a TS expert’s type sludge.

rvz

> I never took Node seriously for backend work—it was always something I reluctantly touched for client-side stuff

That was my initial assessment as well. Anything JavaScript related I stood clear and far away from using it anywhere near backend systems and relegated it into the list of non-serious technologies to stay away from.

> Over time, though, it’s added so many crufts and features that these days, I shudder at the thought of reading a TS expert’s type sludge.

TypeScript just repeated the same issues as CoffeeScript and both JS and TS are just as bad for software anyways.

Go and Kotlin have much better type systems, but the rest of the JS ecosystem just reeks with immaturity.

rednafi

> That was my initial assessment as well. Anything JavaScript related I stood clear and far away from using it anywhere near backend systems and relegated it into the list of non-serious technologies to stay away from.

Same. The frontend can sustain this continuous churn but the backend can't. I go for Go or Python to build backends these days.

baq

TS type sludge is required to make the JS underneath workable.

I'd welcome TS type system in Python, mypy and co. should steal it outright.

benrutter

I'm a Python developer, and use a bunch of types day-to-day, I haven't used TS aside from intermittent curiousity.

Curious what aspects TS has that Python doesn't? (or that Python doesn't do as well)

baq

I really like that TS makes it possible to work with raw objects (~dicts) without having to worry about keys existing or not existing, especially narrowing down via Pick<> and tricks related to impossible combinations via the never type are nice (e.g. if it has key 'a', it can't have key 'b' and vice versa). That said Python's typing doesn't sit still and I tuned out a couple of years ago, so all those things might be possible today via TypedDict, haven't checked).

rednafi

I work with both Python and TS. Python's type hints feel bolted onto the language, whereas TS feels more native. In Python, you have to import a whole bunch of stuff for typing, which adds runtime overhead.

For example, typing a decorator means importing `ParamSpec`, `Callable`, and a bunch of other things. In TS, all of that is available in the global scope, so it’s way less cluttered. Plus, the type system in TS is a lot more powerful. Generic record types are way nicer in TS than in Python.

rednafi

I also work with Python, and I agree that TS has a better type system than Python. However, Python doesn’t require an additional compilation step, which is a win for it. That said, with tools like Bun, Deno, and Node now capable of running TS out of the box, that’s another win for TS.

OscarDC

A particularly ugly but useful feature of "const enum" (sadly, the "const" flavor of enums are not referred to in this documentation), is that it's the only way to declare a compile-time constant in TypeScript.

e.g. for "development" vs "production" environments, you could write a declaration file for each of those envs as such:

  // production.d.ts

  declare const enum ENVIRONMENT {
    PROD = 0,
    DEV = 1,
    CURRENT_ENV = PROD,
  }
And then write in your code something like:

  // some_file.ts
  if (ENVIRONMENT.CURRENT_ENV === ENVIRONMENT.DEV) {
    // do something for dev builds
  }
It will be replaced by TypeScript at compile-time and most minifiers will then be able to remove the corresponding now-dead code when not in the right env.

This is however mainly useful when you're a library developer, as you may not have any "bundler" dependency or any such complex tool able to do that task.

Here, the alternative of bringing a complex dependency just to be able to replace some constants is not worth its cost (in terms of maintenance, security, simplicity etc.), so even if `const enum`s may seem poorly-adapted, they are actually a good enough solution which just works.

Blackarea

Ts enums are unofficially deprecated.

I remember being shocked about it when i heard that on ts-congress by, Nathan Sanders a ts contributor, around 4-5 years ago.

I find the ts-enums incredibly poorly designed and advice my juniors to stay away from them generally.

It's almost similar for interface (https://shively-sanders.com/types-vs-interfaces.html)