DEV Community

Yann L
Yann L

Posted on • Edited on

Typescript enums drawbacks and solutions

Typescript has for years an enum feature that is very similar to the C# one. However, when run, Typescript is Javascript and enum have to be transpiled. The result is not so satisfying than what we could expect as it's very verbose. In this article we'll see why it's so verbose and what other solutions can improve our code.

The problem of TS enums

Typescript's enums are very handy because there's nothing like enum yet in Javascript, probably because a JS object is very close from an enum already. But when we write TS code, the enum informs about readonly state, which is something that can't be done easily from the object (well this is not true anymore... read further).

However, at build time, the TS enum has to be transpiled to an object. The result will be a very verbose transpiled output for something that could be simply an object structure. The developer has then to choose between a lighter output by using an object or a better typing comfort by using the enum type...

Here's a concrete example of enum transpilation in TS
(TSConfig's target = es2017)

export enum EHttpStatusCode {
    Ok = 200,
    BadRequest = 400,
    Unauthorized = 401,
    Forbidden = 403,
    NotFound = 404,
    ServerError = 500,
}
Enter fullscreen mode Exit fullscreen mode

will transpile to the following JS

export var EHttpStatusCode;
(function (EHttpStatusCode) {
    EHttpStatusCode[EHttpStatusCode["Ok"] = 200] = "Ok";
    EHttpStatusCode[EHttpStatusCode["BadRequest"] = 400] = "BadRequest";
    EHttpStatusCode[EHttpStatusCode["Unauthorized"] = 401] = "Unauthorized";
    EHttpStatusCode[EHttpStatusCode["Forbidden"] = 403] = "Forbidden";
    EHttpStatusCode[EHttpStatusCode["NotFound"] = 404] = "NotFound";
    EHttpStatusCode[EHttpStatusCode["ServerError"] = 500] = "ServerError";
})(EHttpStatusCode || (EHttpStatusCode = {}));
Enter fullscreen mode Exit fullscreen mode

Pretty verbose isn't it?

Why does Typescript generate all this code?

The reason for that is Reverse Mapping. In short, the reverse mapping allows you to pass the value to the enum to get a literal value.

The good point of this reverse mapping is that if it was not provided, but we would need it, then we would have to write some logic like following:

function getEnumKeyByEnumValue<T extends {[ index: string ]: string}>(myEnum: T, enumValue: string): keyof T |null {
    const keys = Object.keys(myEnum).filter(x => myEnum[x] == enumValue);
    return keys.length > 0 ? keys[0] :null;
}
Enter fullscreen mode Exit fullscreen mode

There's a cavehat tho, and it's that most of the time we don't need this reverse mapping... So we're basically bloating the bundle with useless code more than we should.

String enums have no reverse mapping

Something interesting to know is that string enums are handled differently by the compiler. If you compare a string enum and another type of enum (ex: number), you'll quickly notice that the transpiled code is different for the string value. It's now a simple enum without reverse mapping, but still it's more verbose than a basic javascript object.

So if you were trying to use reverse mapping on a string enum, then now you know why your code is not working as expected.

Alternatives to enums

Verbosity of the transpilation is something that was heard by the TypeScript team and they now offers alternatives:

The const assertion feature

The const assertion feature will help us keep a lighter bundle, but at the same time it will give us the same typing feature than the enum.

This is how to use it:

  1. declare the object like you would in plain JS
  2. add as const after the declaration
export const httpStatusCode = {
    Ok: 200,
    BadRequest: 400,
    Unauthorized: 401,
    Forbidden: 403,
    NotFound: 404,
    ServerError: 500,
} as const;
Enter fullscreen mode Exit fullscreen mode

What this declaration will do, is create the object, but also inform TS that all the properties and the object are readonly.
Talking about the output bundle, the Javascript transpiled result will be exactly the same but without the as const statement for sure.

However we still miss the typing feature

So now yes we have the enum declaration, but we still miss the Type which will be used by TS to type check our code.

For TS check, we will need 1 more declaration (here below it's an example of how you could try to declare an enum property, but...)

class HttpResponse {
 code: HttpStatusCode = 200;
// other stuff here
}
Enter fullscreen mode Exit fullscreen mode

This won't work because our object is a value and we can't directly use it as a type. So we will need to extract the type from it:

type HttpStatusCodeKey = keyof typeof httpStatusCode;
export type HttpStatusCode = typeof httpStatusCode[HttpStatusCodeKey];
Enter fullscreen mode Exit fullscreen mode

Now we have the real type which is representing the union of the httpStatusCode object's values. And the good news is that this type is only used for building TS and is then removed from the transpiled output. So the only thing JS will have is the object.

On TS side it will then be very easy to type the code like

class HttpResponse {
 code: HttpStatusCode = httpStatusCode.Ok;
// other stuff here
}
Enter fullscreen mode Exit fullscreen mode

Also notice the casing convention. The object const is camelCased (httpStatusCode) because it's a value. On the other hand, the type extracted from the object is PascalCased (HttpStatusCode). So it follows usual convention and is then easy to differentiate type from value object.

Const enums

The const enum is exactly the same syntax than a standard enum, but with a const prefix.

export const enum EHttpStatusCode {
    Ok = 200,
    BadRequest = 400,
    Unauthorized = 401,
    Forbidden = 403,
    NotFound = 404,
    ServerError = 500,
}
Enter fullscreen mode Exit fullscreen mode

However this slight difference will become a huge one in the transpiled output. See here what will come out:


Enter fullscreen mode Exit fullscreen mode

You read it correctly, there's nothing... The reason for that is that the whole enum is now virtual. And when you will use its value, the value will directly be printed instead. It's like a swipe of the enum to a value at compile time.

Conclusion

Typescript enum is a C# flavor brought to Typescript. It's an innocent syntax when you're coming from C# code, but it can be harmful to the end user. The typescript authors are also aware of this pitfall and are doing great work to provide alternate solutions. So if you don't know what to use you can follow this logic:

  1. Do you need reverse mapping? (If you don't know, then the answer is probably "No")
  2. If Yes, and your enum is not about strings, then you'll have to use a standard enum
  3. If No, continue at point 2
  4. Do you need to loop over enum members?
  5. If Yes, then use const assertion
  6. If No, continue at point 3
  7. Use const enum and live happy

Feel free to share your thoughts in the comment 💬 or to like 👍 the post if it was interesting for you.

Top comments (1)

Collapse
 
amandaolsen profile image
amandaolsen

Thank you for this! This is great! Very detailed and easy to understand.