TypeScript enums: use cases and alternatives

(2ality.com)

131 points | by mariuz 13 hours ago

20 comments

  • skrebbel 10 hours ago
    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 10 hours ago
      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 8 hours ago
        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 8 hours ago
          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.
          • WorldMaker 6 hours ago
            At least some of the time an enum doesn't agree with itself it's an import graph issue where the enum is getting imported from more than one place (perhaps because of multiple versions of a dependency in the middle) and the nominal typing is getting overly conservative that those things despite the same contents may be different things. I have a supposition that this is indirectly because what remains of nominal typing is Symbol types and Symbols do have to be extremely careful about import boundaries, especially when bundlers are involved.
    • n144q 8 hours ago
      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 5 hours ago

            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 1 hour ago
          I'm not often given to aesthetic pronouncements regarding code, but I have to agree that is rather ugly looking.
        • n144q 3 hours ago
          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?
          • baobun 1 hour ago
            Because they are nominally typed, which causes issues for users.

            For example, if you already depend on package foo (depending on package baz@^1.0.1, resolving to 1.0.1) and then add a dependency on package bar (depending on package baz@^1.0.2, resolving to 1.0.3), then the same enums from package baz from the two transitive imports are not compatible, since they are not the exact same instance. So TS won't accept a baz enum returned from foo being passed to a function in bar expecting a baz enum. In this example you could fix it by letting the package manager "dedupe" your lockfile since foo could actually happily also use [email protected]. But if the ranges are incompatible, your best hope is aligning resolutions/overrides. Or fall back to forking and patching packages.

            And if you're writing your own library interfacing with baz enums, you have to include the full exact version of the originating package to get the right reference. So if baz also has 200MB of total dependencies, you can't opt out of those if you want to reference that 10-line enum in a function signature. As opposed to interfaces and const-types, which you can just vendor (copy-paste exactly what you want) and TS figures it out. You could break the type out to a subpackage. Not so with enums.

            If you want to extend a union type, you can just add your own with the new element and it will still typecheck. You can not with enums. So you resort to encapsulation or ad-hoc conversion-functions, which gets frustrating and messy very quickly.

            This is only a concern with enums (and classes, where there is good reason for it as the implementation does matter at runtime in a way that enum primitive values do not). The alternatives don't have this issue - as they are structurally typed, TS will "merge" them at type erasure.

            If you are 100% sure that your enums stay private inside your module and are never exposed via references in public APIs, at least you're mitigating much of this. But why paint yourself into that corner? (at the point that readability of comments is a concern I suspect you need to reconsider)

      • mistercow 5 hours ago
        > Not to mention that you can add documentation to each of the entries.

        You can do that with either other solution.

    • leidenfrost 9 hours ago
      I wonder if there's a guide of recommendations about typescript now deprecated features, and its modern equivalents.
      • WorldMaker 5 hours ago
        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 5 hours ago
          isolatedModules doesn't disable enums in general, only exported const enums (which are arguably the most useful form of enums).
      • msoad 9 hours ago
        Yeah they should deprecate namespaces and enums in the next major version... oh wait...

        TypeScript versioning is literally a joke

        • cap11235 2 hours ago
          It's js devs, what do you expect
    • tgv 10 hours ago
      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 10 hours ago
        > 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 10 hours ago
        > 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!

  • msoad 12 hours ago
    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 6 hours ago
      > 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/

    • madeofpalk 10 hours ago
      > 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 10 hours ago
        I think the biggest hurdle in getting something like that to work is how typescript handles the import syntax
        • WorldMaker 5 hours ago
          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 7 hours ago
          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 :)

          • dunham 5 hours ago
            It would be nice if node did tail call optimization, but that seems unlikely at this point (v8 added and then removed it). I've been using bun as a backend for my toy language because of this.
    • n144q 8 hours ago
      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 4 hours ago
        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 7 hours ago
        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 11 hours ago
      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 10 hours ago
        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 7 hours ago
          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.
          • mistercow 6 hours ago
            That doesn't give you a type that you can use for the actual enum values. If you wanted a function argument to take in one of your enum values, you'd still have to use keyof in the signature like:

               function doSomethingWithMyEnum(val: MyEnum[keyof MyEnum])
            
            You could do `val: number`, but now you're allowing any number at all.

            Ultimately, the type syntax in TypeScript is a key part of the language, and I don't think it's unreasonable to expect developers to learn the basic typeof and keyof operators. If we were talking about something wonkier like mapped types or conditional types, sure, it might make sense to avoid those for something as basic as enums.

        • akdev1l 9 hours ago
          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.

          • nosefurhairdo 6 hours ago
            You get used to it! Also much easier to read in an editor with intellisense than your first exposure as plain text. If you're going to spend any considerable amount of time writing typescript, parent's advice is good.
          • mistercow 8 hours ago
            Well, that's basically how you would have done it in vanilla JS before typescript came around. The main awkwardness is the type definition. I often prefer to use a library like type-fest for this kind of thing so you can just say:

                export type MyEnum = ValueOf<MyEnumMapping>;
            
            TypeScript not having enough sugar in its built-in utility types is definitely a fair criticism.

            But more to the point, the above is not usually how you do enums in TS unless you have some very specific reason to want your values to be numbers at all times. There are some cases like that, but usually you would just let the values be strings, and map them to numbers on demand if that's actually required (e.g. for a specific serialization format).

        • Kiro 6 hours ago
          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.
          • mistercow 5 hours ago
            No, this is what I suggest for someone who wants to do something non-idiomatic because they're used to C. What I suggest in most cases is just a string union type.

            Edit: But for what it's worth, yes, the above is still more elegant than enums. The syntax may feel less elegant, but among other things, the above does not depart from the structural type paradigm that the rest of the language uses, all for something as simple as an enum.

          • cap11235 2 hours ago
            Typescript devs love doing anything except developing scripts with types
      • anamexis 10 hours ago
        In that case you can just use object literals `as const`.
    • Klaster_1 11 hours ago
      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 10 hours ago
        > 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 9 hours ago
        Use `const enum Foo`, they leave no traces in the transpiled JS and provide good IDE experience.
    • baq 11 hours ago
      Maybe argue for enum being added to ecmascript instead?
      • mistercow 10 hours ago
        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.
    • bogdan 12 hours ago
      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 10 hours ago
      Doesn't typescript already work with Deno and Bun? How do they do it?
      • msoad 9 hours ago
        by compiling it, which opens a huge can of worms. Deno relies on tsconfig.json configurations for instance
        • WorldMaker 5 hours ago
          Deno bundles a full LSP that will do compilation using its (modified) tsconfig.json-like configurations, but Deno's type remover at runtime is fairly dumb/simple and I believe a simple Rust implementation. Part of what you can't configure in Deno's tsconfig.json-like configuration files are things that keep the type remover simple (such as turning enums back on).
    • girvo 11 hours ago
      Agreed. Its one of my major annoyances with Relay, is that it generates enums.
    • rvz 11 hours ago
      [flagged]
      • Vinnl 11 hours ago
        I can assure you that I can find a way to shoot myself in the foot in any language.
        • rvz 8 hours ago
          With any language, you can (which isn't my point). The point is which one is the easiest and its with anything in proximity of the whole JavaScript ecosystem including TypeScript.

          It really says a lot about how immature it is especially for backend and just by even hearing the complaints about TypeScript 'enums' tells me all I need to know.

          So what other foot-guns have the JS / TS ecosystem have hidden?

      • n144q 8 hours ago
        Dude, why are you here? Your comment is just a (very opionated) rant and not providing any value for anybody in the discussion.
    • diggan 12 hours ago
      > 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 9 hours ago
        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 12 hours ago
        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
        • diggan 12 hours ago
          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.

          • MrJohz 10 hours ago
            Namespaces are already pretty rarely used - mostly in older codebases in development as modern JS modules were still being developed and worked out. The Typescript compiler, for example, recently put a lot of work into getting rid of namespaces in their codebase and got a nice startup time improvement as a result.

            Parameter properties are I think still used quite heavily in Angular, but I don't see them much elsewhere. Again, they're a very old feature, and they don't play well with newer developments in the language (such as native private attributes), so it doesn't seem like much of a problem to avoid them as well.

            The other big TS-only feature is old-style decorators, but that shows the danger of relying too much on this TS-based syntax sugar. Decorators have gone through several revisions, and the version that Typescript implemented is long dead. But a number of codebases are still stuck using this legacy system because it's not compatible with the newer versions of decorators that will (eventually, hopefully) be implemented in browsers. The legacy system is still maintained, I believe, and you can still keep on using it, but you'll not get the benefits of using the same system as the wider Javascript ecosystem, and you'll not get the benefits of having the syntax be native to browsers, when that happens.

            In general, Typescript works best when you use it as simply a type annotation syntax for Javascript, and not as an additional layer of sugar on top of that. And clearly the Typescript developers see things similarly, because they've stopped implementing sugar-like features and have committed to only implementing the stuff that will also be implemented as new features in Javascript.

          • cbovis 11 hours ago
            It's not a hypothetical, it's here in Node 23: https://nodejs.org/docs/latest/api/typescript.html#typescrip....
            • diggan 11 hours ago
              Speaking about the specification, it's a proposal. Yes, some run ahead and implement proposals under experimental flags, doesn't make it any more/less hypothetical as the proposal can still be rejected rather than progressing.
              • mistercow 11 hours ago
                Come on, now.

                > maybe in the future JS runtimes might be able to strip away types for you without a build-step.

                You can't backpedal from that to "speaking about the specification". It's not future JS runtimes. It's a thing you can take advantage of right now.

          • mistercow 11 hours ago
            I think the difference is just that (IME at least) those other features seem a lot more rarely used than enums. Enums are a feature that maps onto a common language concept, and which new TS devs reach for because they give first class support to a very common need. And the fact that you can usually do what they do more elegantly with other features is not as obvious, particularly because you can't do it the same way in a lot of other popular typed languages.

            Parameter properties and namespaces, on the other hand, are kind of wonky TS specific features that nobody expects to be there unless they specifically find them in the docs. Parameter properties offer a little bit of conciseness but don't fill a pressing need. Namespaces solve a problem that just doesn't actually come up that often, and few devs are going to go actively looking for them. Even the example code on typescriptlang.org doesn't show a very compelling case; they show "Validation" used as a namespace for classes that are already namespaced the low tech way by having "Validator" in their names. (Which isn't to say that the feature isn't ever useful; it's just that cases where someone would actually seek it out are niche.)

            All of that is to say that sure, enums are just one of a handful of features that will break in runtimes that simply strip out types, but they're also the main feature that's likely to trip people up.

          • WorldMaker 5 hours ago
            > because maybe in the future JS runtimes might be able to strip away types for you without a build-step.

            Not just the future. Node shipped this with an "experimental flag" in the last LTS and in Current it works without the flag (will ship in the next LTS without a flag). Deno has done something like this for years now, and Bun for nearly as long. The remaining question is if/when Browser support might also exist, which for now remains at Stage 1 discussions with the technical committee (TC-39).

        • awongh 10 hours ago
          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.

          • JimDabell 10 hours ago
            > except in a case down the road, many years from now

            We’re not talking about the distant future. Node shipped its first version supporting type stripping six months ago.

            • awongh 10 hours ago
              I'm not up to date on what people are working on, but I just mean that type stripping is probably not the final solution to a roadmap of node-typescript compatibility?

              That I would imagine there are other features being proposed that will continue to develop this compatibility?

              • JimDabell 6 hours ago
                The difference between type stripping and implementing other behaviour is precisely what is being flagged as a problem with enums in this thread. If you only have type stripping – which is the present day situation for Node users – then using enums will break TypeScript that you would otherwise be able to execute.
          • mistercow 6 hours ago
            Being able to set up a node project like a web server without configuring or maintaining any build steps is pretty nice.
      • jakub_g 12 hours ago
        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 12 hours ago
          Yes, so an experimental flag will be required for use in production, which is a clear reason to not use them.
  • ivanjermakov 12 hours ago
    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 10 hours ago
      > no private properties

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

      • msoad 10 hours ago
        I think they are referring to `class Foo { constructor(private bar: string) }`
      • ivanjermakov 9 hours ago
        I was talking about useDefineForClassFields and Object.defineProperty with which I encountered performance issues.
  • joshstrange 4 hours ago
    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 4 hours ago
      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 4 hours ago
        You can simplify the `SmsType` declaration a bit.

            export type SmsType = typeof SMS_TYPE_LIST[number];
      • joshstrange 4 hours ago
        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 3 hours ago
          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 2 hours ago
      [dead]
  • conaclos 10 hours ago
    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 9 hours ago
      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 9 hours ago
        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 11 hours ago
    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 5 hours ago
      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 3 hours ago
      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.
    • s900mhz 8 hours ago
      Same, I have use enum strings for years and never had an issue.
    • shortrounddev2 6 hours ago
      In typescript, some types are values. Typescript treats enums as types, but they're secretly values. Classes are types and values.
      • ARandumGuy 5 hours ago
        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.
    • Kiro 6 hours ago
      Yeah, people arguing against enums in this thread are doing a really bad job. I haven't seen a single compelling argument.
    • sirlone 2 hours ago
      [dead]
  • mceachen 2 hours ago
    I've found this to be quite ergonomic and functional for handling string enumerations (including completeness in switches and record definitions):

    https://github.com/photostructure/fs-metadata/blob/main/src/...

    Usage:

        export const Directions = stringEnum("North", "South", "East", "West")
        export type Direction = StringEnumKeys<typeof Directions>
    
    (I haven't published this as a discrete npm package--IMHO you should copy and paste this sort of thing into your own tree).
  • OscarDC 5 hours ago
    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.

  • bluelightning2k 12 hours ago
    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 12 hours ago
      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 12 hours ago
    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.

  • wildpeaks 3 hours ago
    I'd advise against using TS enums nowadays because they conflict with a few useful flags (such as "isolatedModule" or Node type stripping).

    A good tsconfig: https://gist.github.com/cecilemuller/80fed1b963171ca4e117f6d...

  • chpatrick 11 hours ago
    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.
  • rednafi 11 hours ago
    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 11 hours ago
      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 10 hours ago
        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 8 hours ago
          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 11 hours ago
        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.
    • rvz 8 hours ago
      > 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.

  • MortyWaves 11 hours ago
    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...

    • recursive 4 hours ago
      If you want to write a type guard, (is this string a `Fruit`?) you're going to need something at runtime.
  • forty 9 hours ago
    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...

  • nick_wolf 10 hours ago
    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.

    • WorldMaker 5 hours ago
      enums and a couple others were a pre-1.0 Typescript feature when Typescript was first trying to figure out how standards following to be and also TC-39 was just "getting the band back together" to start work on what would become ES2015 (aka "ES6").

      Since 1.0 Typescript has been following a plan that every feature needs to be on TC-39's standards track somewhere and since around 2.5/3.0 they've been even more strict that every feature needs to be at least Stage 3 in TC-39's standards track.

      That enums still exist at all is mostly a testament to Typescript's backwards compatibility goals. A lot of 0.7-ish code will still compile today with the right flags and can be usefully upgraded by setting new flags one at a time. TS 0.7 code also won't look like modern Typescript, it's so far away now.

  • bryancoxwell 8 hours ago
    Are there any TS types aside from enums that generate runtime code?
    • WorldMaker 5 hours ago
      There are just a few left. Enums are definitely the most common to find in the wild. The next most common (and a lot further down) is probably `namespace` (fka "inner modules").
  • jffuwaaaasdf 3 hours ago
    enums and namespaces were two mistakes in the early days of typescript.
  • harha_ 5 hours ago
    TypeScript is unnecessarily complex.
    • gavmor 4 hours ago
      Conveniently, it's entirely and discretely optional!

      Or is there critical typing functionality that Typescript accommodates only perversely?

  • shepherdjerred 4 hours ago
    I dislike TypeScript enums for two reasons:

    1. They have a runtime representation unlike most of the rest of TypeScript

    2. They follow nominal typing instead of structural typing, again unlike the rest of TypeScript

    IMO it's best to use string union types instead of enums. If you need to map that to another representation you can use a function or a record.