[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)