DEV Community

Does TypeScript fail at enums?

Alex Lohr on July 04, 2024

The concept of enumerables is quite simple: an item that can represent a finite number of different things. However, the implementation in language...
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
 
rmkane profile image
Ryan Kane • Edited

You could extract the key and value types:

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

type LanguageKey = keyof typeof Language // "Rust" | "FSharp" | "TypeScript"
type LanguageValue = (typeof Language)[LanguageKey] // 0 | 1 | 2

const helloLanguage = (lang: LanguageValue) => {
  console.log(lang)
}

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

Alternatively, you could create some generic type helpers:

type KeyOf<T> = keyof T
type ValueOf<T> = T[keyof T]

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

type LanguageKey = KeyOf<typeof Language> // "Rust" | "FSharp" | "TypeScript"
type LanguageValue = ValueOf<typeof Language> // 0 | 1 | 2

const helloLanguage = (lang: LanguageValue) => {
  console.log(lang)
}

helloLanguage(Language.TypeScript)
Enter fullscreen mode Exit fullscreen mode
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
 
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
 
nickytonline profile image
Nick Taylor

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

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

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
 
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
 
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
 
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
 
lucas-gregoire profile image
Lucas Grégoire

Why not using string enums ?

enum EnumImplementations {
  Rust = 'Rust',
  FSharp  = 'FSharp',
  TypeScript = 'TypeScript',
}

console.log(EnumImplementation.TypeScript); // Logs "TypeScript"
Enter fullscreen mode Exit fullscreen mode
Collapse
 
lexlohr profile image
Alex Lohr

Because it makes no difference whatsoever in relation to the aforementioned shortcomings.

Collapse
 
digitalrisedorset profile image
Herve Tribouilloy • Edited

I see the enum type in Typescript as a helper to prevent Typos as the autocomplete will tell you what constante are available; enum become types in Typescript, meaning they are helpful to narrow down the possible values for a variable.

For instance, the snippet below showcases a way to make them useful:

const request = (url: string, method: "POST" | "GET) => {
// ...
};

becomes

enum Method {
GET = "GET",
POST = "POST"
}
const request = (url: string, method: Method) => {
// ...
};

--> now, the code above ensures not other value of the enum values are possible for the request method.

You're right in that the transpiled code looks ugly. In fact, there is a way to make the transpiled version better by using const enum. But the latter has performance side-effect. Overall, I wanted to add this comment so that we can validate a positive way that Typescript provides to use them

Collapse
 
lexlohr profile image
Alex Lohr

A union type will also provide autocomplete. Re-exporting enums can cause big issues. Const enums not only has performance side effects, but also affects portability of the code.

Collapse
 
digitalrisedorset profile image
Herve Tribouilloy

much appreciated, I will bear in mind the portability issue. Not something I have come across, and it is very insightful.

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