DEV Community

Alex Lohr
Alex Lohr

Posted on • Edited on

Does TypeScript fail at enums?

The concept of enumerables is quite simple: an item that can represent a finite number of different things. However, the implementation in languages are very different. Some languages like Rust and F# allow enum branches to be containers other types. Most of them compile away the enums to simple numbers without leaving a trace of their use in the compiled code. TypeScript... doesn't really do well in comparison.

Consider the following enum:

enum EnumImplementations {
  Rust,
  FSharp,
  TypeScript,
}

console.log(EnumImplementation.TypeScript);
Enter fullscreen mode Exit fullscreen mode

Transpilation to JavaScript will yield the following result:

var EnumImplementations;
(function (EnumImplementations) {
    EnumImplementations[EnumImplementations["Rust"] = 0] = "Rust";
    EnumImplementations[EnumImplementations["FSharp"] = 1] = "FSharp";
    EnumImplementations[EnumImplementations["TypeScript"] = 2] = "TypeScript";
})(EnumImplementations || (EnumImplementations = {}));

console.log(EnumImplementation.TypeScript);
Enter fullscreen mode Exit fullscreen mode

This is a lot of complex code just to give more meaning to three numbers. Even worse, many tree shaking strategies will leave it as is.

What we would actually need for TypeScript to emit (used JSDoc for the type definition):

/**
 * enum EnumImplementation
 * @typedef {(typeof _EnumImplementation_Rust | typeof _EnumImplementation_FSharp | typeof _EnumImplementation_TypeScript)} EnumImplementation
 */
const _EnumImplementation_Rust = 0;
const _EnumImplementation_FSharp = 1;
const _EnumImplementation_TypeScript = 2;
const _EnumImplementation_Keys = ["Rust", "FSharp", "TypeScript"];

console.log(_EnumImplementations_TypeScript);
Enter fullscreen mode Exit fullscreen mode

Not only has that the advantage of being easily tree-shakable, it is also more readable and concise.

That was a long rant, sorry. What's your take on the issue?

Disclaimer: please don't misunderstand me, I really like TypeScript, but this is one of its shortcomings that rather irks me.

Top comments (52)

Collapse
 
zirkelc profile image
Chris Cook

I always use const objects instead of enums

Collapse
 
link2twenty profile image
Andrew Bone • Edited

In ts how do you add the enum to the declaration?

const Language = Object.freeze({
  Rust: 0,
  FSharp: 1,
  TypeScript: 2
});

const helloLanguage = (lang: 0 | 1 | 2) => {
  console.log(lang);
}

helloLanguage(Language.TypeScript);
Enter fullscreen mode Exit fullscreen mode

Like this? Or is there a better way?

Collapse
 
zirkelc profile image
Chris Cook • Edited

You could use const enums which are removed during compilation. For example, this code:

const enum Language {
  Rust,
  FSharp,
  TypeScript,
}

console.log(Language.Rust)
Enter fullscreen mode Exit fullscreen mode

compiles to:

"use strict";
console.log(0 /* Language.Rust */);
Enter fullscreen mode Exit fullscreen mode

Playground

So the enum value (0,1,2...) is inlined into the code during compilation.

However, I avoid enums completely because I don't see a benefit in using enums vs. plain objects defined with as const:

const Language = {
  Rust: "RUST",
  FSharp: "FSHARP",
  TypeScript: "TYPESCRIPT"
} as const;
Enter fullscreen mode Exit fullscreen mode

Sure, it's more verbose, but since these are normal javascript object, I can easily transform them in other shapes and they also work really well in types and type constraints. Here is some more background on this: TypeScript: Objects vs Enums

Thread Thread
 
lexlohr profile image
Alex Lohr

const enums are fine if your code is self-contained, but they are certainly not portable.

Collapse
 
jcdevguru profile image
jcdevguru • Edited

I agree. TS doesn't provide enums in a way that aligns with other languages. A const object comes closer. You can also define a utility type as a list of constants, e g , type Color = 'red' | 'yellow' | 'green';

Collapse
 
vipul_lal profile image
Vipul Lal

But you then miss out on the features offered by modern languages like you must handle all the cases in your switch statement etc.

Collapse
 
zirkelc profile image
Chris Cook

I don’t know what you mean?

Thread Thread
 
guilherme_taffarelbergam profile image
Guilherme Taffarel Bergamin

What they mean is that when you have an enum, the IDE (and some languages) can alert you if you are not using up all possible branches in a switch. Languages like Kotlin will break if you don't offer all branches of the enum in the switch and didn't add a default branch. Ideally, in these languages, instead of adding a default, you should add all the other branches together instead of a default, so if in the future the enum receives a new item, the compiler won't let you proceed unless you check every use of that enum in switches to see if they need a special treatment for that new item.

This type of behaviour breaking at compilation time is one of the reasons some people don't like using builder patterns. The builder will never tell you that you need to add another attribute. Which isn't an issue when you are the sole developer, but it's an issue when you don't know how the next dev will deal with this. We have had this problem more than once in my company. We depend a lot on Lombok to make Java a little more bearable, but the builder pattern, although very useful and satisfying, isn't dummy safe

Thread Thread
 
lexlohr profile image
Alex Lohr

You could also make sure all branches are covered with a union type, so that point is invalid.

Collapse
 
link2twenty profile image
Andrew Bone

I'd honestly not looked at what it compiled to, I'd always imagined it was something like this.

/**
 * Enum for type safe stuff.
 * @readonly
 * @enum {number}
 */
const Language = Object.freeze({
  Rust: 0,
  FSharp: 1,
  TypeScript: 2
})

console.log(Language.TypeScript);
Enter fullscreen mode Exit fullscreen mode

Which feels a bit more natural to me.

Collapse
 
lexlohr profile image
Alex Lohr

You won't need to freeze the object if you assert that it is read only on type level.

Still, the unnecessary level of complexity irks me. Even worse, those types are not even portable. If you re-export them, they may get loaded from two different locations and stop being compatible with one another, even though they are the same type.

Collapse
 
parker_codes profile image
Parker McMullin

The freezing is probably just to prevent modification at runtime instead of treating it as a normal variable, with object keys that could be mutated.

Thread Thread
 
lexlohr profile image
Alex Lohr

They could also unfreeze the object. If they already violate the types, why would they stop there?

Collapse
 
alaindet profile image
Alain D'Ettorre

The good guy Matt Pocock talked about this. The best way yet is to do

const ENUM_IMPLEMENTATION = {
  Rust: 0,
  FSharp: 1,
  TypeScript: 2,
} as const;

type EnumImplementation = typeof ENUM_IMPLEMENTATION[
  keyof typeof ENUM_IMPLEMENTATION
];
Enter fullscreen mode Exit fullscreen mode

notice the as const part, which translates to Object.freeze(). The type is very convenient, you could create a generic type to abstract this a little bit and this "enum" is even a little bit more flexible than Typescript's enums. I've been using this instead of enums for the last 6-9 months now, I'll never use enums again until they're standardized in JS

Collapse
 
cmacu profile image
Stasi Vladimirov • Edited

Am I the only one who finds this self proclaimed TypeScript expert insufferable? Especially when he talks confidently about stuff he has very little or no practical knowledge what so ever.

If there is something we can all agree it’s that the 2 most important qualities of good code are: 1. it does exactly what it’s supposed to do and 2. it’s easy to read and understand by anyone with basic or even no understanding of the subject.

Your example is the exact opposite of these 2 principles. Might be something you want to consider before blindly trusting β€œtech influencers”

Collapse
 
lexlohr profile image
Alex Lohr • Edited

Am I the only one who finds this self proclaimed TypeScript expert insufferable?

I wouldn't call Matt a self-proclaimed TypeScript expert, because that would just be an ad hominem that would reflect badly on me rather than on him.

If there is something we can all agree it’s that the 2 most important qualities of good code are: 1. it does exactly what it’s supposed to do and 2. it’s easy to read and understand by anyone with basic or even no understanding of the subject.

I think good code is a bit more nuanced: the attributes that qualify good code in my understanding are functionality, maintainability and removability. So it has to work, needs to be maintained (which means it is written for humans and represents the least possible complexity required) and has the weakest possible coupling to the rest of the code it is intended to be used with.

Let's look at Matt's example. Does it work? Certainly. Is it written for humans to read and maintain? Mostly. The const assertion and the type are written to satiate TS - but they increase the maintainability. Does it represent the least possible complexity required? I would argue that my solution is slightly less complex, but that's only by a certain margin, so while I would not give him full marks, he still gets a passing grade from me. Has it the weakest possible coupling? The object can easily be replaced by any other objects; however, the identifiers still leak into the code and therefore remain coupled. I would fail him on that one, but two out of three isn't too bad at the end of the day.

So I wouldn't say it is the exact opposite of the best possible solution, because that's what TS enums are at the moment.

Thread Thread
 
cmacu profile image
Stasi Vladimirov • Edited

It does look like you enjoy writing complex things, I will give you that. Your explanation why this is a reasonable solution is almost as confusing as the original suggestion. I can give you A for effort, but the rest still makes no sense.

In the real world it's hard to justify the use of 2 statements (object definition and type inference) to solve only half of the problem enums are designed for. I mean it's in the name, enumerable... How is the object or the type enumerable? Oh yeah, you can write Object.keys() to achieve that. Congrats now you have 3 statements to solve the problem and let's not talk about dependencies between them and how you need to manage 3 separate references in your codebase. Fascinating! You were able to extrapolate a single simple solution into a 3 dimensional problem. Wow. I guess things like performance and scalability (multiplying every enum by 3) are out of scope. That's how you reach the point in the conversation where you need to ask the question "What are we actually trying to do?" and hopefully come to realization that maybe just maybe you need to get out of the rabbit hole.

Thread Thread
 
lexlohr profile image
Alex Lohr

You seem to confuse enumerable with iterable. The latter is not necessarily a property of enums, even though it is nice to have it.

Also, you seem to have misinterpreted my intention. I had hoped tsc could be made to restore the original enum from the type information so it could be made portable beyond just being exported as multiple constants.

I am aware that this is still not an ideal solution, but it is currently the best we can do without creating a new TC39 proposal that does away with the extra complexity.

Lastly, your opinionated and fallacy-laden discussion style detracts from your criticism.

Thread Thread
 
alaindet profile image
Alain D'Ettorre

@cmacu I agree you're creating more references in the code with Matt's suggestion, but that still is the worst thing it's happening for me, nothing worse. Moreover, you're just creating a simple and friendly JS literal object in the transpiled code, as the type goes away, while the native "enum" approach creates some ugly IIFE with double the declared properties in the enum instead (check TypeScript Playground's compiled JS panel on the right).

I think we can all agree that native enums would be much better and solve the problem of having a somewhat weird syntax with as const like this, but for now that is the "most performant way" and also the most JS-friendly way I'd say.

Marginally, enums declared like this both provide the benefit of typing, avoiding literal values in code but also you can use its type in a little more forgiving way. Take this for example

type EnumLike<T = any> = T[keyof T];

const LANGUAGE= {
  Rust: 'rust',
  FSharp: 'fsharp',
  TypeScript: 'typescript',
} as const;

type Language = EnumLike<typeof LANGUAGE>;

function selectLanguage(lang: Language) {
  console.log('selected language is: ', lang);
}

selectLanguage('typescript');
selectLanguage(LANGUAGE.FSharp);
Enter fullscreen mode Exit fullscreen mode

Notice that I can call selectLanguage() with both a literal value (maybe coming from external sources like an HTTP request or database) and with the pseudo-enum without being forced to the use real enum, when using the enum as a type. For me, it's a nice thing to have

Thread Thread
 
cmacu profile image
Stasi Vladimirov

OK, I had to a double take and according to all definitions I found enumerable means exactly what I was referring to. The ability to be counted/named in orderly fashion (one by one). Which indeed is a subset of iterable given that there are various ways to iterate over collection and is supported by many other data structures. But back to enums. I don't see the fallacy in the statement that neither object as const nor the inferred type fully supports the simple and much needed functionality of enums. I am glad that we both agree on that. Also I don't understand how ideal solutions, TC39 proposals, etc are relevant in a discussion of comparing 2 solutions aiming to solve a basic problem. We could as well just employ other languages or tools for that purpose, which is way beyond the point. I simply stated that the solution you are referring to (and which was suggested by a third party with no credentials) is a non starter. I provided basic and solid justifications to which you responded with dismissive statements and unrelated tangents.

And to make things clear. I think the issues developers are experiencing with enums are simply due to incorrect usage. Often the discussion involves hypothetical scenarios only possible due to fallacy-laden decisions. Enums are widely used without an issue in every major open source project including the TypeScript Compiler and VSCode which are directly supported by Microsoft. Are there trade-offs and concerns? Of course! Give me example of anything that doesn't have issues...

This becomes a real problem when people on the internet start advocating for alternative solutions which don't solve the problem, add additional complexity and have not been battle tested in large codebases. Especially when that's seldomly done just because its promoted by influencers for the purpose of demonstrating expertise as an ability to understand or create complexity. In other words posting BS online and others spreading it...

Thread Thread
 
lexlohr profile image
Alex Lohr

Nobody claimed their solution was perfect, just less bad than the one currently existing in TS. This is the original point of this discussion. In most languages, enums do not fulfill your definition without extra work. JS does not even support them as part of the language.

Both proposed solutions are clear improvements over what TS currently delivers. That is the point of both them and this post. Your points, while being valid out of context, reframe the whole discussion and would be more apt in a discussions how native ECMAscript enums should look like.

If you don't understand that much, I have nothing more to gain from this discussion and will stop answering.

Thread Thread
 
cmacu profile image
Stasi Vladimirov

What is bad about TS enums? If you the transpiled code bothers you you can use string enums. String enums transpile to basic objects. What kind of argument is "JS does not even support them"? JS does not support as const either, nor a bunch of other things. So what? Let's make this easy and simple: What is the problem specifically with string enums? Please provide a reference, not an opinion but a real reference/example of string enums being bad or having issues. You want to keep things objective and make me understand. Give me a proof of your claims without shifting the subject.

Thread Thread
 
lexlohr profile image
Alex Lohr

It's you who is shifting the subject. This was decidedly not about string enums. If I wanted to use strings, I'd just use them and a union type, because strings provide meaning themselves. The main point about enums in TS is that they allow you to assign meaning and structure to otherwise meaningless numbers. You may lament they don't do more than that, but that's again you trying to evade the topic, then.

Thread Thread
 
cmacu profile image
Stasi Vladimirov

Ok, so looks like you are saying that enums in TypeScript are doing exactly what they are supposed to do. I didn’t see any arguments why enums are bad. Give me a reason why enums are bad and I should not use them and look for alternative solutions that have their own issues.

Thread Thread
 
lexlohr profile image
Alex Lohr

I mentioned the issues portability and tree shaking. I don't say you shouldn't use them, just that their implementation is worse than it should be.

Thread Thread
 
cmacu profile image
Stasi Vladimirov

The as const solution doesn't support tree shaking either. Nor even the individual object keys. If you import the type, the object definition has to be imported too, because it's dynamic. Same with any exported iterable value... And how is that more portable? Which part is exactly is portable in your solution?

As previously said. Everything has trade offs. Everything. Calling it bad and posting online how people should stop using it is BS. Especially when the proposed solution is at least as "bad" and comes with it's own can of worms. For someone claiming decades of experience in web development you should know better than making such claims.

Can you show me example in a major open source codebase (or your own) where anyone uses as const and type inference exclusively instead of enums?

Thread Thread
 
lexlohr profile image
Alex Lohr

As const needs no tree shaking, but has zero portability. My solution does, but lacks extensive support by tsc.

Nobody uses as const in major code bases because it is not portable. However, I've seen multiple code bases that consciously omitted the use of enums for the reasons I have named.

Thread Thread
 
cmacu profile image
Stasi Vladimirov

Which codebase has omitted the use of enums? Can you send me some references? I am curious to learn the reasoning as well as what solution they went with.

I’ve been searching unsuccessfully for examples for years at this point. I’ve tried some alternative approaches too and I always come back to enums. But I also really dislike β€œmagic string” solutions so there’s that.

Thread Thread
 
lexlohr profile image
Alex Lohr

Sorry, all closed source products. I prefer "magic numbers" (numeric variables with speaking names) over "magic strings", but whenever I encounter enums in external libraries I'm working with, I avoid re-exporting them and instead add a JSdoc comment pointing out the correct import out of courtesy for the next poor sod who has to use them.

Collapse
 
nickytonline profile image
Nick Taylor

This is true for enums by default in TypeScript, but I typically opt for const enums instead.

Collapse
 
matveit profile image
ΠœΠ°Ρ‚Π²Π΅ΠΉ Π’

My method is lengthy, but also arguably the most powerful:

namespace EnumImplementation {
    export type Rust = 0;// Can be anything
    export const Rust: Rust = 0;// Sadly explicit type is required, otherwise typechecking will fail 
    export type FSharp = false;// This can be same type, or different, doesn't matter
    export const FSharp: FSharp = 1;
    export type TypeScript = "Worst";
    export const TypeScript: TypeScript = "Worst";
}

type EnumImplementation = EnumImplementation.Rust | EnumImplementation. FSharp | EnumImplementation.TypeScript;
Enter fullscreen mode Exit fullscreen mode

Note: in all of my code bases I do not use TypeScript's built in enums for aforementioned issues, and other minor implementation inconsistencies. The above method is more powerful as it allows for inline enum variant selections (instead of EnumImplementation.Rust you can just put 0, and so on) and doesn't feature any other hidden traps during runtime.

Collapse
 
sjiamnocna profile image
Šimon Janča

Oh yes. I found out my favourite optimization (from C), that makes the code more effective by replacing strings with numbers (number comparison is always faster than eg. string), is not an optimization at all in Typescript.

So I use constants.

Collapse
 
hesxenon profile image
hesxenon

I discourage the use of enums whenever possible for a few reasons, them being unnecessary as the primary one. Typescript really doesn't need them and depending on the "guarantees" you can make about a code base (immutability etc.) something like defining an object as const and then inferring the types from that is a lot more practical

Collapse
 
jangelodev profile image
JoΓ£o Angelo

Hi Alex Lohr,
Top, very nice and helpful !
Thanks for sharing.

Collapse
 
guilherme_taffarelbergam profile image
Guilherme Taffarel Bergamin

I always thought they were syntax sugar for constants. I guess I'll use more true constants then.

Collapse
 
adaptive-shield-matrix profile image
Adaptive Shield Matrix

Typescript enums are deprecated and should not be used anymore.
Const objects are the way to go in typescript/javascript.

import { getObjectValues } from "@/utils/obj/getObjectValues.ts"
import { z } from "zod"

export type Region = keyof typeof region

export const region = {
  us: "us",
  eu: "eu",
} as const

export const regionSchema = z.enum(getObjectValues(region))

export function getObjectValues<T extends Record<string, any>>(obj: T) {
  return Object.values(obj) as [(typeof obj)[keyof T]]
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
adaptive-shield-matrix profile image
Adaptive Shield Matrix

If you need 1 or more value mappings you use:

export const regionName = {
  us: "USA",
  eu: "Europa",
} as const satisfies Record<Region, string>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
sirajulm profile image
Sirajul Muneer

Typescript enums aren’t deprecated. Can you share me an official statement from typescript on it? They are only discouraged for use by one side of the community, there are lovers for enums too.

Collapse
 
adaptive-shield-matrix profile image
Adaptive Shield Matrix

Even on the official typescript website it lists problems, pitfalls using enums (instead of using const objects)
typescriptlang.org/docs/handbook/e...

and it does not even touch frameworks, babel, and other build tools that all had (or still have) problems with typescript enums.

If you run your code everywhere besides your own dev machine you have to care about how it gets build, deployed and run by the user.

If you have 2 possible solutions, one of which is completely problem free and the over is fraught by countless pitfalls -> it may not be officially sunsetted, but is a bad choice to make in any case.

Even official maintainers/creators of typescript regret the creation of enums.

Hence my TLDR summary: enums are deprecated

Thread Thread
 
jason_efstathiou_47a00fda profile image
Jason Efstathiou • Edited

On the official docs it only lists pitfalls with const enums, which do not apply to enums generally. And even the const enum pitfalls, as it clearly says, only apply if you're emitting or consuming d.ts files.

and it does not even touch frameworks, babel, and other build tools that all had (or still have) problems with typescript enums.

What problems with frameworks or build tools are you referring to exactly? Personally in years of working with typescript and many different frameworks, and some pretty wild build pipelines, I don't think I recall ever having any issues related to enums specifically.

If you run your code everywhere besides your own dev machine you have to care about how it gets build, deployed and run by the user.

Sure but what does this have to do with ... enums? How would enums even have any effect on how your code gets deployed and ran by the user

If you have 2 possible solutions, one of which is completely problem free and the over is fraught by countless pitfalls

Again, what "countless pitfalls" are you talking about...

Even official maintainers/creators of typescript regret the creation of enums.

Citation needed

Hence my TLDR summary: enums are deprecated

Well you're wrong though. They're not deprecated.

Thread Thread
 
guilherme_taffarelbergam profile image
Guilherme Taffarel Bergamin

It may have problems, yes, but as far as I know it's not deprecated. And officially deprecated is the only kind of deprecated. You could say it's a bad practice to use it in your point of view, but you can't say it's deprecated. Deprecation is a way to say "this should be done differently and may be removed in future versions". I don't see that movement from Typescript.

Collapse
 
adaptive-shield-matrix profile image
Adaptive Shield Matrix
Collapse
 
jason_efstathiou_47a00fda profile image
Jason Efstathiou

The first point here is just plain wrong, or maybe outdated β€” numbers are NOT accepted for numeric enum arguments. Give it a try on the TS playground right now.

The second argument makes no sense. If you use an enum as an arg type, you can't pass a string... Yeah, of course. Because that's the whole point of enums. You can change the string value of an enum member once, and all the literals change everywhere.

Thread Thread
 
lexlohr profile image
Alex Lohr

I have taken the code directly from the TS playground. And I'm afraid you misunderstood my second argument. It's not about the tooling, but about the resulting code.

Thread Thread
 
jason_efstathiou_47a00fda profile image
Jason Efstathiou

Hey, my comment wasn't about your article, but the one linked in the comment I replied to.