[HN Gopher] An ode to TypeScript enums
       ___________________________________________________________________
        
       An ode to TypeScript enums
        
       Author : disintegrator
       Score  : 49 points
       Date   : 2025-03-02 17:23 UTC (5 hours ago)
        
 (HTM) web link (blog.disintegrator.dev)
 (TXT) w3m dump (blog.disintegrator.dev)
        
       | bubblyworld wrote:
       | One thing I find useful about enums is that they can be used as
       | both types and values, which is ergonomic for decorator-based
       | libraries (like class-validator, nestjs, mikro-orm, etc). The
       | best approach I've found in union land is using _const_
       | assertions and _typeof_ , which I don't love.
       | 
       | Agree with the author that in almost every other way unions are
       | better though... they play much more nicely with the rest of the
       | type system. I find it endlessly annoying that I have to refer to
       | enum members directly instead of just using literals like you can
       | with union types.
        
         | disintegrator wrote:
         | > One thing I find useful about enums is that they can be used
         | as both types and values
         | 
         | Makes sense. You can emulate that behavior by having an object
         | literal with const assertion AND a union type of the same name
         | derived from the object literal.
        
           | bubblyworld wrote:
           | Right, yeah - this is what I meant by _const_ and _typeof_.
           | It 's definitely an option, but I'm nervous of relying on the
           | semantics of _const_ like that. But maybe I shouldn 't be, it
           | seems pretty idiomatic?
           | 
           | (the _typeof_ part is just so you don 't repeat yourself, or
           | did you have something else in mind?)
        
       | wruza wrote:
       | Parameter properties also gone? I only recently found out about
       | these, so useful for data-ish classes.
        
         | disintegrator wrote:
         | In principle you'll still be able to use all of the features
         | that existed before this flag but you'll need to compile the
         | code if targeting Node.js. I do think that this new flag is
         | going to draw people away and we'll probably see a bunch of
         | tsconfig presets and boilerplate projects setting it to true.
         | 
         | If you're using a bundler then your're not to going benefit
         | from it in the medium term. It's possible this will unlock
         | faster build times with them in the future.
        
       | lelandfe wrote:
       | > _TypeScript 5.8 is out bringing with it the
       | --erasableSyntaxOnly flag_
       | 
       | TypeScript sure loves the "our only documentation lives in the
       | changelog" approach to stuff, huh?
       | 
       | - The on-site Algolia search returns 0 results for
       | "erasableSyntaxOnly"
       | 
       | - The blocked-from-search release notes[0] looks like actual
       | documentation - but urges to check out the PR[1] "for more
       | information," despite the PR description being essentially blank.
       | 
       | - The CLI options page[2] describes it thus: "Do not allow
       | runtime constructs that are not part of ECMAScript," with no
       | links to learn more about what that means.
       | 
       | Edit: actually, I take it back! Clicking the flag on the CLI page
       | takes one to an intimidating junk drawer page... but my issues
       | with discoverability stand:
       | https://www.typescriptlang.org/tsconfig/#erasableSyntaxOnly
       | 
       | [0] https://www.typescriptlang.org/docs/handbook/release-
       | notes/t...
       | 
       | [1] https://github.com/microsoft/TypeScript/pull/61011
       | 
       | [2] No idea why on-site search doesn't pick this up:
       | https://www.typescriptlang.org/docs/handbook/compiler-option...
        
         | Tadpole9181 wrote:
         | It happened within hours, it can take a bit to update the docs
         | and then get them reindexed.
        
         | MBCook wrote:
         | It was released late last week wasn't it?
        
       | homebrewer wrote:
       | const enums are almost never mentioned by these articles for some
       | reason. They give you the best of both worlds: they're fully
       | erasable, and have good LSP support (do no need to search for
       | strings and bump into false matches -- or even worse, for
       | numbers).
        
         | furstenheim wrote:
         | Lack of LSP support looks really bad in the article proposed
         | solution :/.
         | 
         | But const enum seems to have several pitfalls.
         | https://www.typescriptlang.org/docs/handbook/enums.html
        
         | krona wrote:
         | > const enums are almost never mentioned by these articles for
         | some reason.
         | 
         | I think it's because a lot of tooling (excepting TSC) doesn't
         | support cross-file const enums. But I agree - it's one of the
         | reasons I started using TypeScript way back in 2013. I wouldn't
         | be able to write comprehensible performance sensitive code
         | without it.
        
       | zdragnar wrote:
       | Seeing posts like these, I often feel alone preferring enums to
       | string unions.
       | 
       | There are certain situations where refactoring a string in a
       | union will not work but refactoring an enum will. I don't want to
       | type strings when, semantically, what I want is a discrete type.
       | I don't even care that they become strings in JS, because I'm
       | using them for the semantic and type benefits, not the benefits
       | that come with the string prototype.
        
         | forty wrote:
         | That precisely one of my problem with enum: almost all TS type
         | is structural typing, why have this exception enums being
         | nominal typing?
        
           | Tade0 wrote:
           | And to add to the confusion Template Types let you compare
           | enums as if they were strings.
        
           | zdragnar wrote:
           | Classes aren't interchangable, excepting using a child when a
           | parent is called for.
           | 
           | Likewise, enums represent a discrete and unique set. The fact
           | that there is either a number or a string used under the hood
           | is irrelevant.
           | 
           | I imagine using numbers or strings was useful for interop
           | with vanilla JS (where JS needs to call a TS function with an
           | enum as an argument), so it makes sense to use it instead of
           | Symbols, which is what I typically pretend enumd are.
        
         | bubblyworld wrote:
         | Interesting point about semantics. I wish there was a way to
         | get the best of both - discrete type (correct semantics) but
         | one that is auto inferred from literals in contexts where the
         | type system expects it (ergonomics of use). Perhaps there are
         | good reasons that doesn't work though, I haven't thought
         | through it much =P
        
         | eyelidlessness wrote:
         | You're not alone! I've given up the preference on team projects
         | for pragmatic reasons, but the semantics of (string) enums are
         | still my personal preference.
        
       | forty wrote:
       | This is my preferred home made way of doing "Enum" in TS theses
       | days
       | https://gist.github.com/forty/ac392b0413c711eb2d8c628b3e7698... -
       | it includes syntax to migrate from TS enum.
       | 
       | The member documentation point is a good one, I'll look what can
       | be done with my solution.
        
         | krona wrote:
         | My enums are almost always numbers, not strings.
        
           | forty wrote:
           | It should be supported by the above code
        
       | spankalee wrote:
       | Const objects really are better than enums, in every way except
       | declaration brevity.
       | 
       | They're erasable syntax, so they work in environments that just
       | strip types. Their emit is just what you write without the types.
       | They can be composed in a type-safe way with standard JS
       | operations.
       | 
       | You can still write JS docs for values, deprecated the, mark them
       | as internal, etc.                   type ValueOf<T> = T[keyof T];
       | const Foo = {           /**            * A one digit            *
       | @deprecated            */           one: '1',           two: '2',
       | three: '3'         } as const;         type Foo = ValueOf<typeof
       | Foo>;              const Bar = {           blue: 'blue',
       | } as const;         type Bar = ValueOf<typeof Bar>;
       | // You can union enum objects:         const FooOrBar = {...Foo,
       | ...Bar};         // And get union of their values:         type
       | FooOrBar = ValueOf<typeof FooOrBar>;              const
       | doSomething = (foo: Foo) => {}              // You can reference
       | values just like enums:         doSomething(Foo.two);
       | // You can also type-safely reference enum values by their
       | // key name:         doSomething(Foo['two']);
       | 
       | Given the TypeScript team's stance on new non-erasable syntax, I
       | have to think this is how they would have gone if they had `as
       | const` from the beginning. Ron Buckton of the TS team is
       | championing an enum proposal for JS:
       | https://github.com/rbuckton/proposal-enum Hopefully that goes
       | somewhere and improves the declaration side of thigns too.
        
         | hassleblad23 wrote:
         | Its a shame because I like the enum way of declaration a lot
         | more.
         | 
         | `const Foo = { Bar: 'bar' } as const` - this just feels a bit
         | weird.
        
           | spankalee wrote:
           | That's a taste thing. Personally, I like my TypeScript as a
           | superset of JS with types, so I dislike all the custom value-
           | space syntax.
           | 
           | `const Foo = { Bar: 'bar' }` is how I would write an enum-
           | like object in JS, so that's how I want to write it in
           | TypeScript, just with added types.
        
           | DidYaWipe wrote:
           | Yes, why do we have "const" at the beginning AND end?
        
             | moogly wrote:
             | The `as const` at the end will ensure the type of `Foo` is
             | not widened to a `string`.
        
         | disintegrator wrote:
         | This is spot on but the issue I called out in my post is that
         | there's nothing drawing devs to import the mapping. Like
         | there's the immediate convenience of passing a string literal
         | to a field that's a string union for instance. You've done a
         | nice thing in your snippet and named the mapping and
         | corresponding type the same but that's also uncommon though I'm
         | seeing it more nowadays. So as I see it it's very possible to
         | end up with a codebase that inconsistently uses the object
         | mapping if that matters to you.
        
           | spankalee wrote:
           | I don't know how important it is to bad use of the values
           | directly, but that is also possible.
           | 
           | You have to intersect every value with a brand, like:
           | type Enum<T> = {           [K in keyof T]: T[K] & {__brand:
           | never};         }              const _Foo = {           one:
           | '1',           two: '2',           three: '3'         } as
           | const;         const Foo = _Foo as Enum<typeof _Foo>;
           | type Foo = ValueOf<typeof Foo>;
           | 
           | And now, this will work:
           | doSomething(Foo.two);
           | 
           | But this will error:                   doSomething('2');
        
         | cies wrote:
         | When I see this it makes me want to run for
         | ReasonML/ReScript/Elm/PureScript.
         | 
         | Sum types (without payloads on the instances they are
         | effectively enums) should not require a evening filling
         | ceremonial dance event to define.
         | 
         | https://reasonml.github.io/
         | 
         | https://rescript-lang.org/
         | 
         | https://elm-lang.org/
         | 
         | https://www.purescript.org/
         | 
         | (any I forgot?)
         | 
         | It's nice that TS is a strict super set of JS... But that's
         | about the only reason TS is nice. Apart from that the "being a
         | strict super set" hampers TS is a million and one ways.
         | 
         | To me JS is too broken to fix with a strict super set.
        
           | dkersten wrote:
           | > (any I forgot?)
           | 
           | Gleam has real sum types and can compile to JS.
           | 
           | https://gleam.run/
        
           | spankalee wrote:
           | > It's nice that TS is a strict super set of JS... But that's
           | about the only reason TS is nice.
           | 
           | I mean, yes, exactly?? That's TypeScript's entire reason for
           | being, and it's no small thing.
           | 
           | I use TypeScript where I would have used plain JavaScript. If
           | I have a reasonable choice of an entirely different language
           | - ie, I'm not targeting browsers or Node - then I would
           | definitely consider that.
           | 
           | I personally haven't seen that _any_ compile-to-JS language
           | is worth the interop tax with browsers or the JS ecosystem,
           | and I 've built very complex apps on GWT and used to be on
           | the Dart team working on JS interop.
        
             | Freedom2 wrote:
             | Civet (https://civet.dev) is probably my favorite one if I
             | want something a bit fancier than Typescript, purely
             | because it shares the same elements that you are as "opt-
             | in" as much as you like, at least in my limited experience.
        
           | hombre_fatal wrote:
           | On the other hand, pasting the `type ValueOf<T> = T[keyof
           | T];` idiom into your TS code so you can use it for your enums
           | is a hell of a lot less ceremony than ditching TS for any of
           | the languages you listed. Especially when you can still just
           | us TS enums if you wish.
           | 
           | And on top of that, each of them has a whole new collection
           | of ceremonies you're going to have to learn.
           | 
           | All for what, to avoid `as const`?
        
           | pjmlp wrote:
           | That single reason is all that matters, because it maps
           | directly to what the platform actually understands, instead
           | of adding another layer to debug.
        
           | ninetyninenine wrote:
           | TS sum types are actually more powerful thanks to 'as const'
           | 
           | These are dependent types which none of the languages above
           | can enable. Meaning the type system can actually read values
           | in your code and create types from the code. This is not
           | inferring the type, this is very different.
           | 
           | For example:                  const PossibleStates = ["test",
           | "me"] as const             type SumTypeFromArray = (typeof
           | PossibleStates)[number]             let x: SumTypeFromArray =
           | "this string triggers a type error as it is neither 'test'
           | nor 'me'"
           | 
           | So in TS you can actually loop through possible states while
           | in ML style languages you would have to pattern match them
           | individually.
        
           | int_19h wrote:
           | TS does have sum types, so you can already do something like:
           | type Color = "red" | "green";
           | 
           | What GP is doing is some scaffolding on top to make the
           | values more discoverable and allow associating arbitrary
           | Color-specific metadata with them.
        
         | fixprix wrote:
         | TypeScript/ES6 is such a great language with a feature set far
         | ahead of other languages in many ways. The lack of enums though
         | is a sore spot. I really hope that proposal you mentioned can
         | move forward.
         | 
         | Also you can improve your implementation with
         | Object.freeze(Foo) and { one: Symbol("1") }
        
       | DanielHB wrote:
       | General programming languages theory question, is one supposed to
       | iterate over enum entries or is that considered an antipattern? I
       | have found myself needing to do that a few times and it always
       | felt a bit dirty.
        
         | panstromek wrote:
         | I wouldn't say it's an antipattern. E.g. listing all possible
         | values of some user input field is a pretty natural use case
         | for that.
        
         | williamdclt wrote:
         | No that's fine and a reasonable thing to do . In fact, I'd say
         | it is one of the main points of enums and one of my biggest
         | gripes against Go is the lack of that capability
        
           | floydnoel wrote:
           | Swift can't handle it either, I had to write an extension to
           | add that ability. Felt weird to me.
        
       | MBCook wrote:
       | What do people find works better as a string enum replacement?
       | const Thing {         one: "one",         two: "two",
       | three: "three"       } as const
       | 
       | Or just                 type Thing = "one" | "two" | "three"
       | 
       | I've been thinking of getting rid of the simple string enums I
       | have but it's not clear to me why one is preferred over the other
       | by people.
        
         | braebo wrote:
         | If all you need is a union type then the latter is plenty.
         | 
         | If you need the actual strings to iterate over or validate
         | against, deriving the value from an const array is helpful:
         | const THINGS = ['one', 'two', 'three'] as const            type
         | Thing = THINGS[number]
        
       | o11c wrote:
       | The problem with "just use literal strings/numbers" is that
       | that's exactly the _opposite_ of type safe. With them it is
       | impossible to specify an argument of type `myenum | number |
       | string`, despite that being commonly desired in some form.
       | 
       | When targeting javascript, it seems to me that the obvious
       | approach is to use _symbols_ for enums. But symbols have a lot of
       | WET.
       | 
       | (of course, typescript's safety is unfixably broken in numerous
       | other ways, so why bother?)
        
       | motorest wrote:
       | Can anyone explain why enums are somehow bad but literal unions
       | are supposed to be good?
       | 
       | I'll be blunt: at the surface level, it looks like literal unions
       | are something that only someone with an irrational axe to grind
       | against enums would ever suggest as a preferable alternative just
       | to not concede that enums are fine.
       | 
       | If the problem lies in the low-level implementation details of
       | enums, I cannot see any reason why they shouldn't be implemented
       | the same way as literal unions.
       | 
       | So can anyone offer any explanation on why enums should be
       | considered bad but literal unions should be good?
        
         | moogly wrote:
         | TypeScript enums require codegen, which won't work in a type
         | erasure world. This is explained in the article.
        
       | DidYaWipe wrote:
       | "Probably my favorite argument in steelmanning enums"
       | 
       | Whatever that's supposed to mean.
        
         | crummy wrote:
         | Steelmanning is the opposite of strawmanning.
         | 
         | In other words, it's making the strongest version of an
         | argument for the opposing side of the argument. The author
         | doesn't like enums but is talking about their best attributes.
        
           | DidYaWipe wrote:
           | Thanks for the reply. I read a lot, and I've never
           | encountered this term before. Seems like "in defense of"
           | would be every bit as good, and universally understood.
        
             | swatcoder wrote:
             | It's an internet term of recent origin, from a specific
             | community, not a traditional one.
             | 
             | It's good to be familiar with the word, as it comes up in
             | adjacent communities like this one, but like with most
             | slang, there are indeed clearer ways to say the same thing.
             | 
             | But also, some people don't realize that they've picked up
             | a slang term or that people outside their community are
             | part of discussions like we have here, so it comes up a
             | lot. Now that you've spotted it, you'll likely see it here
             | a lot.
             | 
             | (FWIW, I hate it and am grateful that nobody can see me
             | roll my eyes when its used. Same for "motte and bailey" and
             | other comically pseudo-erudite slang from those folks)
        
       ___________________________________________________________________
       (page generated 2025-03-02 23:01 UTC)