DEV Community

Alex Lohr
Alex Lohr

Posted 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?

Top comments (26)

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

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
 
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
 
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.

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
 
nickytonline profile image
Nick Taylor

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

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
 
ferdous_shareef_ebc32075d profile image
Ferdous Shareef • Edited

I understand your frustration with TypeScript enums. The concept of enumerables is straightforward, but the implementation can vary significantly across languages. TypeScript's transpilation to JavaScript does result in more verbose code compared to some other languages like Rust and F#.

Consider the example you provided:

enum EnumImplementations {
Rust,
FSharp,
TypeScript,
}

console.log(EnumImplementations.TypeScript);
Transpilation to JavaScript results in:

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

console.log(EnumImplementations.TypeScript);
This is quite a bit of code just to map three numbers to their names, and it does make tree shaking less effective.

A more concise and tree-shakable approach could look like this:

javascript

/**

  • 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(_EnumImplementation_TypeScript);
This approach is not only more concise but also more readable. It would be great if TypeScript could offer a more optimized way to handle enums in the future.

For anyone interested in exploring more about TypeScript and its intricacies, I highly recommend checking out Codeflee, which provides excellent resources and tutorials on web development and TypeScript.

What's your take on this? Have you found any workarounds or alternatives that work better in your projects?

Collapse
 
martinmcwhorter profile image
Martin McWhorter

Why not just use const enums which do compile away into literals?
typescriptlang.org/docs/handbook/e...