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.

leidenfrost

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

msoad

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

TypeScript versioning is literally a joke

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.

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!

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.

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

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

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.

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.

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.

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.

rererereferred

Doesn't typescript already work with Deno and Bun? How do they do it?

msoad

by compiling it, which opens a huge can of worms. Deno relies on tsconfig.json configurations for instance

diggan

> 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]

How is that the conclusion you reach? The proposal you link says types will be treated like comments by the runtime, so it's not about adding types that will be used in the runtime (which begs the question, why even add it? But I digress), but about adding types that other tooling can use, and the runtime can ignore.

So assuming the runtime will ignore the types, why would using enums specifically break this, compared to any other TypeScript-specific syntax?

moogly

I banned enums in TS codebases I contributed to _over 6 years ago_ when Babel 7 w/ TS transpilation support came out, and namespaces along with old TS modules even earlier than that when moving over to JS modules.

If you ask me, both features have been de facto deprecated for ages now.

The future has been here for a while.

msoad

The idea from the proposal is that types are used by other tools to type-check and runtimes would ignore them. It's not final yet but it's very likely that in future you can `node -e 'function foo(arg: string) {}; foo(42)'` but if your code has `enum` in it, Node.js or browser will throw an error

awongh

I get why people would want to push this forward in general, but except in a case down the road, many years from now, is there a real case right now for running your typescript code without compiling it?

Maybe library compatibility?

My first reaction is that this just further fractures the ecosystem, where some codebases/libraries will have TS that is required to be compiled and some will not, adding a third kind of TS/JS code that's out there.

diggan

But it's not specifically about enums, but anything from TS that generates code. You would need to stop using enums, parameter properties, namespaces (called out by the proposal) and probably more.

Seems weird to me to decide you're OK with the build step and all the other complexity TS adds, but using enums is too much, because maybe in the future JS runtimes might be able to strip away types for you without a build-step.

But we all have different constraints and use cases, I suppose it does make sense for what you're building.

jakub_g

https://nodejs.org/docs/latest/api/typescript.html#typescrip...

> Since Node.js is only removing inline types, any TypeScript features that involve replacing TypeScript syntax with new JavaScript syntax will error, unless the flag --experimental-transform-types is passed.

> The most prominent features that require transformation are:

> Enum > namespaces > legacy module > parameter properties

lost_womble

Yes, so an experimental flag will be required for use in production, which is a clear reason to not use them.

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.

ivanjermakov

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

msoad

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

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.

forty

After several iterations (some of which older than TS native enums if I remember well), this is the Enum code I ended up with. It creates type, "accessors" (`MyEnum.value`), type guard (`isMyENum(...)`) and set of values (`for(const value of MyEnum)`), and have 2 constructor to allow easier transition from TS native enum.

https://gist.github.com/forty/ac392b0413c711eb2d8c628b3e7698...

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.

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.

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?

nick_wolf

This article could be an unintentional case study in why letting patterns emerge beats designing them upfront. Java devs insisted on enum classes while JS devs gravitated towards plain objects tells us something about language evolution.

Makes me wonder if it was a mistake to include them at all instead of letting the community converge on patterns naturally, like we did with so many other JS patterns.

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.

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)

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.

MortyWaves

Had a quick look but I was surprised to see using a Set.

Personally I use a plain string union. If I need to lookup a value based on that I’ll usually create a record (which is just a stricter object). Typescript will error if I tried to add a duplicate.

This is all enforced at build time, whereas using a Set only happens at runtime.

    type Fruit = ‘apple’ | ‘banana’;

    const lookup: Record<Fruit, string> = { ‘apple’: ‘OK’, ‘banana’: ‘Meh’ }
Unions are a more more universal syntax than enums.

It isn’t forced to be a 1:1 map of string to string; I’ll often use string to React components which is really nice for lots of conditional rendering.

On a slightly related topic, I also feel that the ‘type’ keyword is far more useful and preferable than ‘interface’. [1]

[1]: https://www.lloydatkinson.net/posts/2023/favour-typescript-t...