DEV Community

Cover image for We don't have to use Enums on TypeScript? 【Discussion】
Taishi
Taishi

Posted on • Edited on

We don't have to use Enums on TypeScript? 【Discussion】

Hi, Devs! I am Taishi, a software developer in Vancouver!

I have been thinking about using Enums lately, and my conclusion is that we should NOT use Enums if possible.

But why?

Enums has impact on compiled JavaScript files

Usually tsc command removes all the TypeScript unique stuff such as

  • Type definition
  • Interface definition
  • Type annotation

This means TypeScript doesn't have any effect on the compiled JavaScript files.

On the other hand, TypeScript has some impact on JS files if you use Enums.

Let's compare them!

Case 1: Union types

TypeScript

type BigTechz = "Google" | "Apple" | "Amazon";

const myFav: BigTechz = "Apple";
Enter fullscreen mode Exit fullscreen mode

Javascript after tsc

"use strict";
const myFav = "Apple";
Enter fullscreen mode Exit fullscreen mode

Case 2: Enums

enum BigTechz {
  Google = "Google",
  Apple = "Apple",
  Amazon = "Amazon",
}

const myFav: BigTechz = BigTechz.Apple;
Enter fullscreen mode Exit fullscreen mode

Javascript after tsc

"use strict";
var BigTechz;
(function (BigTechz) {
  BigTechz["Google"] = "Google";
  BigTechz["Apple"] = "Apple";
  BigTechz["Amazon"] = "Amazon";
})(BigTechz || (BigTechz = {}));
const myFav = BigTechz.Apple;
Enter fullscreen mode Exit fullscreen mode

My thought

As you can see above, Enums stays on a JavaScript file as an object...which causes a bigger bundle size.
And more importantly, I think TypeScript shouldn't have any impact on compiled JavaScript files.

TypeScript is basically a type checker when you write codes and when you compile TS files with tsc command.

Most of the times you can use other TS syntax instead of Enums, and I think we should do that if possible (Maybe I just prefer Union types🙄)!

I'd like to hear other devs' opinion about this topic, and I appreciated if you leave a comment!

Thanks!

Top comments (33)

Collapse
 
peerreynders profile image
peerreynders • Edited
const bigTechz = {
  Google: 'Google',
  Apple: 'Apple',
  Amazon: 'Amazon',
  Facebook: 'Meta',
} as const;

type TypeBigTechz = typeof bigTechz;
type BigTechz = TypeBigTechz[keyof TypeBigTechz];

const myFav: BigTechz = bigTechz.Apple;
Enter fullscreen mode Exit fullscreen mode

Discussion

"The biggest argument in favour of this format over TypeScript’s enum is that it keeps your codebase aligned with the state of JavaScript"

Collapse
 
taishi profile image
Taishi

Thanks @peerreynders !

This is cool.

However, I probably go with Union types in this case since it saves some lines of codes and has more readability in my opinion.

Collapse
 
peerreynders profile image
peerreynders • Edited

We start with

const bigTechz = {
  Google: 'Google',
  Apple: 'Apple',
  Amazon: 'Amazon',
  Facebook: 'Facebook',
} as const;
Enter fullscreen mode Exit fullscreen mode

Then 2021-10-28

const bigTechz = {
  Google: 'Google',
  Apple: 'Apple',
  Amazon: 'Amazon',
  Facebook: 'Meta',
} as const;
Enter fullscreen mode Exit fullscreen mode

No need to change bigTechz.Facebook anywhere in your code and more importantly in code you don't control (but that uses your code).

With

type BigTechz = "Google" | "Apple" | "Amazon" | "Facebook";
Enter fullscreen mode Exit fullscreen mode

the literal values are used in the source code itself. So

  • either update all occurrences of "Facebook" in the source code; impossible if it is used in code you don't control.
  • or just leave it as is and hope nobody complains that it should be "Meta", not "Facebook".

Trade-offs ...


Added bonus: iterability

const allBigTechz = Object.values(bigTechz);

// Using type predicates
// https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
//
function isBigTechz(name: string): name is BigTechz {
  return (allBigTechz as string[]).includes(name);
}

const myFav = bigTechz.Apple;
console.log(isBigTechz(myFav)); // true
console.log(isBigTechz('Walmart')); // false

const formatter = new Intl.ListFormat('en', {
  style: 'long',
  type: 'conjunction',
});
console.log(`The BigTechz: ${formatter.format(allBigTechz)}`); 
// "The BigTechz: Google, Apple, Amazon, and Meta"
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
taishi profile image
Taishi

Thanks @peerreynders !

No need to change bigTechz.Facebook anywhere in your code and more importantly in code you don't control (but that uses your code).

Good point!
iterability can be a big plus.
Now your method is starting to look handy! Thank you!

Collapse
 
haaxor1689 profile image
Maroš Beťko • Edited

I prefer union types over this since it's much less code and is still completely type safe because of how string literal types work. Another benefit over the object example is that you only need to import the type which will not be in the final JS package.

If you don't need to iterate over the options, all you need is union of string literals.

type BigTech = 'Google' | 'Meta';

const foo: BigTech = 'Google';
Enter fullscreen mode Exit fullscreen mode

If you need to iterate over the options, there is only one small change needed.

const bigTechz = ['Google', 'Meta'] as const;
type BigTech = typeof bigTechz[number];
Enter fullscreen mode Exit fullscreen mode

Only use case where I would use the object approach would be if I had an enum of number values that would benefit from using named options, or if I had string values that can be renamed so they are easier to read.

Collapse
 
peerreynders profile image
peerreynders

since it's much less code

I feel this gets bandied about a lot in TypeScript circles as if "less code" is always an absolute net win under all circumstances—perhaps it's an over-generalization/simplification of the "less code, less bugs" slogan or some kind of hangover from the verbosity of Java.

Whether or not "less code" is an advantage is, as with everything, highly context sensitive.

  • less code is an advantage if no capabilities are lost and no information is obscured.
  • less code is a trade off if capabilities are reduced or information is obscured. Now any one individual may not care about the capabilities or information that are being lost but those preferences can be subjective.

For example, in general one is encouraged not to specify the return type of a function in TypeScript. I argue that the return type of a function is part of the function's type and as such is part of its design and should therefore be made explicit (and Rust seems to have the same idea).

Effective Typescript (p.85):

"Similar considerations apply to a function’s return type. You may still want to annotate this even when it can be inferred to ensure that implementation errors don’t leak out into uses of the function."

(The fact that an IDE will show the return type is irrelevant if most of your code reading occurs on Github).

Also note that when you use a union:

const foo: BigTech = 'Google';
Enter fullscreen mode Exit fullscreen mode

i.e. you need to declare the TypeScript type otherwise TypeScript will assume that it is dealing with a primitive string. Compare that to:

const foo = bigTechz.Google;
Enter fullscreen mode Exit fullscreen mode

i.e. with the "JavaScript-style object namespacing a group of values approach" TypeScript has all the information it needs to know what is going on—it isn't necessary to supply TypeScript specific information.

So while the union may be less verbose at the site of declaration there is little difference at the site of use.

Now I suspect that I'm in the minority because I prefer the JavaScript version but I don't use TypeScript as a language but as a JavaScript dialect that occasionally makes type information more explicit so that "TypeScript the super-linter" can do its job, so I like to stay as close as possible to the "source material"—though occasionally I may choose to leave explicit type information for my own benefit.

Collapse
 
ecyrbe profile image
ecyrbe

I also use this pattern all the time

Collapse
 
nikolaymatrosov profile image
Nikolay Matrosov

Have you heard about const enum? It transplies to values only. E.g.

const enum Test {
    FOO = 'foo',
    BAR = 'bar'
}

const s = Test.BAR;
Enter fullscreen mode Exit fullscreen mode

results in

const s = "bar" /* BAR */;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
taishi profile image
Taishi

This is cool. I just prefer Union types but this is great if this Enums gets stripped out when compiling! Thanks @nikolaymatrosov !

Collapse
 
firstwhack profile image
FirstWhack • Edited

You can't index a union type unless you're creating a lookup table based on the union so "prefer unions" is simply unviable. This is what TS does but TS can remove that lookup at compile time for "const" Enums.

Don't reinvent core features.

Collapse
 
pontakornth profile image
Pontakorn Paesaeng

I think it's neat. ReScript also uses this approach. Also, you need a plugin if you use Babel.

Collapse
 
thethirdrace profile image
TheThirdRace

I think @peerreynders pretty much nails why enums are useful, but to add a bit...

The bigger JS bundle size argument is a fact, but then again the impact is so minimal it's like arguing an operation is faster than another when both executes over 1M times per second... performance is not affected in the slightest, it's a non-issue.

I've seen you argue in the comments that union types are more readable, I would argue they're perfectly readable, but so are enums. I would be hard pressed to declare a winner based on readability.

The biggest concern for me, and why I mostly never use union type to create a list of values, is how limiting it can be for your code:

  • Not all editor will allow you to rename a union type value by pressing F2 like VS Code. Some editors will force you to do a search and replace, which is very prone to errors because not all instances found must be replaced...
  • When you have a monorepo with 10+ packages, renaming a value in the union type might not be a huge challenge, but it takes a lot more time to push the new packages to PROD. Not to mention the coordination and/or downtime required for everything to be updated before your system is ready to manage the renamed value. I've seen systems with more than 30 packages very intrinsically linked, union types would be a nightmare...
  • You don't always have a monorepo... Sometimes you're dealing with 30 separate repos, renaming a union type value is NOT a viable option

While I understand it would be better that Typescript doesn't affect the transpiled JS, there are specific reasons why enums exists in so many languages. These reasons are very valid to a point where union type should probably not be used as list of values. Don't get me wrong, there are very real usecases for union type, I'm just saying a list of values might not be the best choice.

While the current Typescript implementation is a bit lacking, enums have many advantages that will make your life much easier while maintaining a real system in production. I would also argue that all the downsides are pretty insignificant when looking at the big picture.

Food for thoughts about the bundle size... You never hear anybody complain about classes in JS increasing the bundle size, but this is a fact. A class cannot tree-shake any of its methods and the sheer amount of developers that insist on using classes everywhere instead of functions is astonishing. Classes will increase your bundle size many order of magnitude more than you could ever achieve through enums. And yet there are tons of posts about not using enum and absolutely none about not using classes unless really needed...

Collapse
 
peerreynders profile image
peerreynders

i.e. if bundle size is a concern don't use TypeScript at all; instead use JS Doc TS like Preact does.

Collapse
 
taishi profile image
Taishi

Thanks for bringing up some very useful points @thethirdrace !

You and @peerreynders 's comments have made me almost fall in love with Enums tbh 😳

Collapse
 
bobdotjs profile image
Bob Bass

I just answered a question on quora last week where someone was asking why we need enums. It was a good question so I did a little bit of research first.

It turns out that the data type was originally created when we had far less computing power and integers were used as identifiers for things that we would use hash tables or switch statements for today. The purpose was to make the code more readable during some of the most primitive periods of computer programming.

I'm a software engineer and these days I'm working mostly within the JavaScript ecosystem. I write a lot of TypeScript and I'm a firm believer that you should only use language features when it makes sense to do so, you shouldn't use them just because they exist. You should always choose the option that is the most readable, clean, and organized for your particular use case.

I can't even tell you the last time I used an Enum in production. There is really just no good reason. I was racking my brain as I tried to come up with a good use case for the person who asked the question. The only things that I could come up with were so convoluted that it made me realize that I don't need Enums, not with the modern web application work I'm doing these days.

It's possible that you might use an Enum if you are converting a value from MySQL from tinyint into a Boolean, but I'd rather use a ternary operator instead.

If someone can think of a good use case, I'd really love to hear it.

Collapse
 
errorgamer2000 profile image
ErrorGamer2000 • Edited

Similar to Volodymyr Yepishev, I personally think that they are extremely useful for error codes, because you could seperate them into named groups that are easily readable. This would probably be the most useful in larger libraries.

Collapse
 
jackmellis profile image
Jack

While I agree that some usecases for enums could be replaced with union types, I can think of a few where enums are a much better fit.

enum ApiKeys {
  Facebook: 'somereallylongkeythatyouwouldntwanttohavetotypeoutorrepeatinmanyplaces',
}
Enter fullscreen mode Exit fullscreen mode
enum Network {
  Mainnet: 1,
  Poygon: 137,
  Rinkeby: 4
}
Enter fullscreen mode Exit fullscreen mode

I've seen many cases where we've had a collection of long, ugly, or cryptic strings. Unlike the keys of an enum, a union type does nothing to help describe what a certain string is actually for. Another fairly common usecase is when we have numeric values, if I had to type the network number every time I would waste so much time having to double check the numbers are correct...

Collapse
 
jkierem profile image
Jkierem • Edited

In my opinion, normally, one more object in memory is not enough to be considered a problem. As with everything, enums serve a purpose: having a type with restricted runtime representation. Unions serve this purpose too but are based around literals. So unions can be seen as anonymous enums.
Personally, I would like to have haskell/rust/scala enums, where the members of the enum can store values and could be destructured

Also keep in mind there is a proposal for enums in JS so this overhead of TS adding may become valid JS

Collapse
 
peerreynders profile image
peerreynders • Edited

That proposal has been inactive since 2018 and isn't referenced anywhere on the TC39 stages - so it's unlikely to go anywhere.

Another proposal seems to be in the works but isn't a match with the contemporary TypeScript version (which could be deliberate; ADT support).

Collapse
 
pmejna profile image
Przemyslaw Mejna

There is some truth to that but from my perspective enums are still useful. As an example: you might use enums in the TypeORM, working with MySQL database. There is no coincidence that they are here and they are in many programming languages. There is a lot of usecases like that. And to be honest - usually you use enums In like 0.1% of your codebase so worrying about space in that particular case is pointless.

Collapse
 
abhinav1217 profile image
Abhinav Kulshreshtha

Your case is valid if the variable being used is a constant, in that case a const enum would be best of both world.

But imagine you are writing a game, and game states needs to be manipulate at runtime. In that case, it would be required to bundle entire object in final build. An Enum of game state would be better, especially if you are making a game library, which would be used by other developers ( who may be an old fashion JS devs).

Collapse
 
abhinav1217 profile image
Abhinav Kulshreshtha • Edited

Real use case scenario of union types would be something like this

type StringOrNumber = string | number;
let code: StringOrNumber;
code = 123;   // OK
code = "ABC"; // OK
code = false; // Compiler Error
Enter fullscreen mode Exit fullscreen mode
Collapse
 
paratron profile image
Christian Engel

Thats because you can also use enums to address things and use them as object keys for example.

For me thats not a problem, as long as the developer is aware of that.

The overhead added by them won't have a considerable impact on bundle size and/or performance.

Collapse
 
taishi profile image
Taishi

Thanks!

The overhead added by them won't have a considerable impact on bundle size and/or performance.

I know this but TypeScript shouldn't have any impact on complied JS files (as an object in Enums' case) in my opinion.
This is not just a file size issue.

Collapse
 
bwca profile image
Volodymyr Yepishev

Well, if I had numerical values representing some status codes, I would prefer them in an enum, since that would be more declarative than a bunch of magic numbers, but that's just my preference 🥴