7 ms·
TypeScript enums: use cases and alternatives
- msoad 2y agoAfter 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/PTAEFEA8EMFsAcA2BTUBGAXKAqgZwJYB2A5qAK6H4D2hoALgJ7zK6hG53LQAmoVAZqGSEysXAChGzUAGU60TqAC8oAEQBBAMZ18AN2SrQAHzUBJQtG16DAbnHj+FKzVCIqxOQuQAKDl6yenACUoADe4qCRoJo0uFQoAHRuxN4ABgAqABaommQATnnCdKB+ivi4WAAkoaXIAL6pQXZ19iA4uNDEyOLJgT4aVvqqTT3ufd6qAArC3ETEwzagbeAFVHlY6UyoAORTM3Oq22yshFTF0LgExBYARij0VPRboNt92wmtYFBwSKgATFg8HNQNBorF5IRilQbgArZDaUAAd3wdEyoFSFzBhA4qXEMWxxT6yjCESiWh0+iwAwpBgANKTIuZLDSqUzBnTxHUQax8Rw7JJnn0ANLIBisFQAa1FAiezBlfUWbWp1kMJlUbJpqgF0j6ADVoIgyCxid4pMh5fJggBtYWi3AAXUVYGVQ2MZgs7K1DicOhcvUtyAAYnkqLAAPKw+F0XwBgIB-WGlghcJRLFxRLJNJZHL5QqQkoB45VGoBhojFriNp4Trdf1eYOhiNw7TePoJcnWEZ1zgN8ORlt7QizEgLJZfVbrUCbaS7aZDg5HcqgU7nS74a7QO6oOiPM0vF0GN3qj2a96fCAwBD3ADMWAAcqJkHl8JohCJYKBCkhLMhYEU8eCoAPn+z6aESKgpmS7JYGg9Kphq1hYAADPSXKYrydD8nuwFPi+eoGka4qgKaWwyjhoF9EEVpSgwMpmmRj4UQGjpjugbpIfYjiEM4tDJOReEBjG-hAYxAleAmhHJgyabxMgSTuFm2TLqJr61EWoDVLUZbNOe1ZdKMxD8WBglGW2Hb6F27imYJfxNKxKwhpO047H8i4nGc3JXLc9w7rKOxIW6aBnpWXyXr8oAACxYAAstA8DwMC764a+awFs+JCgLoBHGr5mSiNAhAALSFDwm73BYf4SBh7RPgASrJxKQZE6jcLARBUuoAAi0WmHeqhwVEeBPlS2AyOAtX9dJADihF0FSU3YOAMjpJNaE8uCWHPENeT1SgEnGioJFyoI227cgVE0XRpEnbgdWySxSpdT1fVHqN42qmoC1LStnE+tQtBdHQp2yd4IYoICt07bJ+24FJqa4MidCaGioOyXDqZRJoFyoMDiQtW1hAYNJGNRIUdD5LQGitUQ5R0HkChrKodgk5EWO3TVUOJNtRMsxjZMU2onhDtAeS8NtTPE5j2Mc2dCQzSwc2SyT-N5JT8scBzEupi0FZVh0+m8rJ8kpIDuM+Gb7bU4QQR2W0+M0xw9M7nkAHYkbmam5DZ0TGGADqd7vTbToQBOGzPLsfsBxNbnLh5FxeWV267uHGjdb1H2qG9E1Hl9y2HB8IUXj89wAKxYJ1DAVS+oA0awZAEBlqSXfwqQgkOfklNA-DIIwrsawAwvEayxfAjXSbVyDcFSADEgaBkhC9IZNqZTYUwgzwvc8L8vUQAEKJhvi9z6t3Jpph9h7oPbh5ERzd+TKV-D3FweqBP3AZ6vyDCBn+9Gl6XE8VAIDR+eQAASyBIDeBiNfLAIDYYklTCrWgICR5WmgWsR0nJdL626IbDMClgFDzARAiYb9hi2zALPeei8+7uwIT3EB4DIGqAAJrIEQG4RE5Dg4OTWGHGcrD2GcMOMcWOq4E5bgeB3XYZCc5r0ID-RM+dxBAA https://www.typescriptlang.org/play/?#code/PTAEFEA8EMFsAcA2B... [1] https://github.com/tc39/proposal-type-annotations https://github.com/tc39/proposal-type-annotations
- diggan 2y ago> Enums is going to make your TypeScript code not work in a future where TypeScript code can be run with Node.js or in browser when typings are added to JavaScript[1] How is that the conclusion you reach? The proposal you link says types will be treated like comments by the runtime, so it's not about adding types that will be used in the runtime (which begs the question, why even add it? But I digress), but about adding types that other tooling can use, and the runtime can ignore. So assuming the runtime will ignore the types, why would using enums specifically break this, compared to any other TypeScript-specific syntax?
- msoad 2y agoThe 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 2y agoBut 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 2y agoIt's not a hypothetical, it's here in Node 23: https://nodejs.org/docs/latest/api/typescript.html#typescript-features https://nodejs.org/docs/latest/api/typescript.html#typescrip....
- 2y ago
- bluelightning2k 2y agoI 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 2y agoI’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 2y agoI 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 2y agoI 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-essentials/typescript-only-features https://www.totaltypescript.com/books/total-typescript-essen... TS has a great type system, the rest of the language is runtime overhead.
- preommr 2y ago> no private properties Private properties have been in the works for the last 7-8 years, and were officially added three years ago.
- msoad 2y agoI think they are referring to `class Foo { constructor(private bar: string) }`
- ivanjermakov 2y agoI was talking about useDefineForClassFields and Object.defineProperty with which I encountered performance issues.
- rednafi 2y agoFor 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 2y agoTS 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 2y agoI 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 2y agoI'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 2y agoI 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).
- MortyWaves 2y agoHad 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-types-over-interfaces/ https://www.lloydatkinson.net/posts/2023/favour-typescript-t...
- recursive 2y agoIf you want to write a type guard, (is this string a `Fruit`?) you're going to need something at runtime.
- Aeolun 2y agoI 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 2y agoSame, I have use enum strings for years and never had an issue.
- shortrounddev2 2y agoIn typescript, some types are values. Typescript treats enums as types, but they're secretly values. Classes are types and values.
- ARandumGuy 2y agoHow 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 2y agoYeah, 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 2y agoI 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/ 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 2y ago
- chpatrick 2y agoI 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.
- pspeter3 2y agoAre all strings under a certain length interned?
- skrebbel 2y agoOne 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 2y agoChanging 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 2y ago> Changing values (after a change in an external interface), tracking use and renaming is harder in the first case. You can rename the elements of a string union with the typescript language server. In VS Code at least, it's just like renaming a variable, and it updates the usages which use the type. > In the second case, the code can change the value at runtime. You can always freeze the object if you're worried about that.
- skrebbel 2y ago> Changing values (after a change in an external interface), tracking use and renaming is harder in the first case. FWIW in VS Code I can rename a string literal (in the type definition) and it's renamed everywhere. Similarly I can use "Find All References", it just works. Pretty cool!
- mistercow 2y agoI 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.
- nick_wolf 2y agoThis 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 2y agoenums 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 2y agoThe 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/Reference/Global_Objects/Object/proto https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... [1] https://github.com/microsoft/TypeScript/issues/59658 https://github.com/microsoft/TypeScript/issues/59658
- moogly 2y agoRe: __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 2y agoThanks 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 2y agoAfter 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/ac392b0413c711eb2d8c628b3e769896 https://gist.github.com/forty/ac392b0413c711eb2d8c628b3e7698...
- bryancoxwell 2y agoAre there any TS types aside from enums that generate runtime code?
- WorldMaker 2y agoThere 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 2y agoA 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_ 2y agoTypeScript is unnecessarily complex.
- gavmor 2y agoConveniently, it's entirely and discretely optional! Or is there critical typing functionality that Typescript accommodates only perversely?
- harha_ 2y agoSure, it is optional, but I've seen the unnecessary complexity seep into codebases over time, even though the software was originally elegant and simple. I'm not sure if such perversions exist, it's been a while already since I touched TypeScript. To me the whole system seems perverted in the sense that an inexperienced developer can easily make things unnecessarily complex.
- codelikeawolf 2y agoYou are preaching to the choir. I have been using TypeScript for around 7 years, and it's great in a lot of ways, but unnecessary complexity is everywhere in the TS world. I regularly see elaborate type acrobatics that kneecap TSServer because of recursive types and complex inference. It would be fine if it was just hard to read/understand, but it can have profound negative DX consequences. I once encountered some complex types that were causing tsc to take over 5 minutes to run on a reasonably large codebase (~200K lines). Removing them dropped the type-checking time down to 2 minutes. I wish devs would stop worrying about making _everything_ type-safe and just write a few damn unit tests.
- joshstrange 2y agoWe 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 2y agoExactly 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 2y agoNice, 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 2y agoI 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 2y agoYou can simplify the `SmsType` declaration a bit. export type SmsType = typeof SMS_TYPE_LIST[number];
- shepherdjerred 2y agoI 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.
- deleted 2y ago[deleted]
- wildpeaks 2y agoI'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/80fed1b963171ca4e117f6d1e2a4a5a7 https://gist.github.com/cecilemuller/80fed1b963171ca4e117f6d...
- jffuwaaaasdf 2y agoenums and namespaces were two mistakes in the early days of typescript.
- mceachen 2y agoI'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/string_enum.ts 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).
- Blackarea 2y agoTs enums are unofficially deprecated. I remember being shocked about it when i heard that on ts-congress by, Nathan Sanders a ts contributor, around 4-5 years ago. I find the ts-enums incredibly poorly designed and advice my juniors to stay away from them generally. It's almost similar for interface (https://shively-sanders.com/types-vs-interfaces.html https://shively-sanders.com/types-vs-interfaces.html)