[HN Gopher] TypeScript enums: use cases and alternatives
       ___________________________________________________________________
        
       TypeScript enums: use cases and alternatives
        
       Author : mariuz
       Score  : 133 points
       Date   : 2025-01-20 09:30 UTC (13 hours ago)
        
 (HTM) web link (2ality.com)
 (TXT) w3m dump (2ality.com)
        
       | msoad wrote:
       | 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
        
         | diggan wrote:
         | > 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?
        
           | msoad wrote:
           | 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 wrote:
             | 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.
        
               | cbovis wrote:
               | It's not a hypothetical, it's here in Node 23: https://no
               | dejs.org/docs/latest/api/typescript.html#typescrip....
        
               | diggan wrote:
               | 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 wrote:
               | 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 wrote:
               | 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.
        
               | MrJohz wrote:
               | 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.
        
               | WorldMaker wrote:
               | > 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 wrote:
             | 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 wrote:
               | > 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 wrote:
               | 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 wrote:
               | 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 wrote:
               | Being able to set up a node project like a web server
               | without configuring or maintaining any build steps is
               | pretty nice.
        
           | jakub_g wrote:
           | 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 wrote:
             | Yes, so an experimental flag will be required for use in
             | production, which is a clear reason to not use them.
        
           | moogly wrote:
           | 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.
        
         | bogdan wrote:
         | 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 wrote:
         | 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 wrote:
           | > 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 wrote:
           | Use `const enum Foo`, they leave no traces in the transpiled
           | JS and provide good IDE experience.
        
         | baq wrote:
         | Maybe argue for enum being added to ecmascript instead?
        
           | mistercow wrote:
           | 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.
        
         | girvo wrote:
         | Agreed. Its one of my major annoyances with Relay, is that it
         | generates enums.
        
         | FjordWarden wrote:
         | 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.
        
           | anamexis wrote:
           | In that case you can just use object literals `as const`.
        
           | mistercow wrote:
           | 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.
        
             | akdev1l wrote:
             | 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.
        
               | mistercow wrote:
               | 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).
        
               | nosefurhairdo wrote:
               | 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.
        
             | eeue56 wrote:
             | 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 wrote:
               | 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.
        
             | Kiro wrote:
             | 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 wrote:
               | 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 wrote:
               | Typescript devs love doing anything except developing
               | scripts with types
        
         | madeofpalk wrote:
         | > 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 wrote:
           | I think the biggest hurdle in getting something like that to
           | work is how typescript handles the import syntax
        
             | madeofpalk wrote:
             | 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 wrote:
               | 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.
        
             | WorldMaker wrote:
             | 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/#verbatimModule
             | Synta...
        
         | rererereferred wrote:
         | Doesn't typescript already work with Deno and Bun? How do they
         | do it?
        
           | msoad wrote:
           | by compiling it, which opens a huge can of worms. Deno relies
           | on tsconfig.json configurations for instance
        
             | WorldMaker wrote:
             | 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).
        
         | n144q wrote:
         | 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...
        
           | runarberg wrote:
           | 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.
        
           | eyelidlessness wrote:
           | 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.
        
         | throwitaway1123 wrote:
         | > 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 wrote:
           | 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/#verbatimModuleSy
           | nta...
           | 
           | [2] https://www.typescriptlang.org/tsconfig/#isolatedModules
        
             | throwitaway1123 wrote:
             | 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...
        
               | WorldMaker wrote:
               | Also worth noting that the eslint rules for what
               | `erasableSyntaxOnly` might be are already well
               | established, for those looking to do it today.
        
       | bluelightning2k wrote:
       | 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 wrote:
         | 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 wrote:
       | 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.
        
       | ivanjermakov wrote:
       | 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 wrote:
         | > no private properties
         | 
         | Private properties have been in the works for the last 7-8
         | years, and were officially added three years ago.
        
           | msoad wrote:
           | I think they are referring to `class Foo {
           | constructor(private bar: string) }`
        
           | ivanjermakov wrote:
           | I was talking about useDefineForClassFields and
           | Object.defineProperty with which I encountered performance
           | issues.
        
       | rednafi wrote:
       | 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 wrote:
         | 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.
        
           | rednafi wrote:
           | 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.
        
           | benrutter wrote:
           | 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 wrote:
             | 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).
        
         | rvz wrote:
         | > 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 wrote:
       | 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 wrote:
         | If you want to write a type guard, (is this string a `Fruit`?)
         | you're going to need something at runtime.
        
       | Aeolun wrote:
       | 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?
        
         | s900mhz wrote:
         | Same, I have use enum strings for years and never had an issue.
        
         | shortrounddev2 wrote:
         | In typescript, some types are values. Typescript treats enums
         | as types, but they're secretly values. Classes are types and
         | values.
        
           | ARandumGuy wrote:
           | 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 wrote:
         | Yeah, people arguing against enums in this thread are doing a
         | really bad job. I haven't seen a single compelling argument.
        
         | y-c-o-m-b wrote:
         | 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 wrote:
         | 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.
        
       | chpatrick wrote:
       | 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.
        
       | skrebbel wrote:
       | 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.
        
         | tgv wrote:
         | 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 wrote:
           | > 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 wrote:
           | > 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 wrote:
         | 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 wrote:
           | 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 wrote:
             | 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 wrote:
               | 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.
        
         | leidenfrost wrote:
         | I wonder if there's a guide of recommendations about typescript
         | now deprecated features, and its modern equivalents.
        
           | msoad wrote:
           | Yeah they should deprecate namespaces and enums in the next
           | major version... oh wait...
           | 
           | TypeScript versioning is literally a joke
        
             | cap11235 wrote:
             | It's js devs, what do you expect
        
           | WorldMaker wrote:
           | 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/tsc
           | onfig/#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 wrote:
             | isolatedModules doesn't disable enums in general, only
             | exported const enums (which are arguably the most useful
             | form of enums).
        
         | n144q wrote:
         | 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.
        
           | mistercow wrote:
           | > Not to mention that you can add documentation to each of
           | the entries.
           | 
           | You can do that with either other solution.
        
           | WorldMaker wrote:
           | 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.
        
             | n144q wrote:
             | 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 wrote:
               | 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 baz@1.0.3. 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)
        
             | bryanrasmussen wrote:
             | I'm not often given to aesthetic pronouncements regarding
             | code, but I have to agree that is rather ugly looking.
        
       | nick_wolf wrote:
       | 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 wrote:
         | 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.
        
       | conaclos wrote:
       | 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 wrote:
         | 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 wrote:
           | 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 wrote:
       | 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...
        
       | bryancoxwell wrote:
       | Are there any TS types aside from enums that generate runtime
       | code?
        
         | WorldMaker wrote:
         | 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").
        
       | OscarDC wrote:
       | 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.
        
       | harha_ wrote:
       | TypeScript is unnecessarily complex.
        
         | gavmor wrote:
         | Conveniently, it's entirely and discretely optional!
         | 
         | Or is there critical typing functionality that Typescript
         | accommodates only perversely?
        
       | joshstrange wrote:
       | 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 wrote:
         | 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];
        
           | joshstrange wrote:
           | 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 wrote:
             | 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)
        
           | recursive wrote:
           | You can simplify the `SmsType` declaration a bit.
           | export type SmsType = typeof SMS_TYPE_LIST[number];
        
       | shepherdjerred wrote:
       | 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.
        
       | wildpeaks wrote:
       | 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...
        
       | jffuwaaaasdf wrote:
       | enums and namespaces were two mistakes in the early days of
       | typescript.
        
       | mceachen wrote:
       | 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).
        
       ___________________________________________________________________
       (page generated 2025-01-20 23:01 UTC)