Typescript enums drawbacks and solutions
Yann L
Posted on December 22, 2021
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,
}
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 = {}));
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;
}
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:
- declare the object like you would in plain JS
- add
as const
after the declaration
export const httpStatusCode = {
Ok: 200,
BadRequest: 400,
Unauthorized: 401,
Forbidden: 403,
NotFound: 404,
ServerError: 500,
} as const;
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
}
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];
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
}
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,
}
However this slight difference will become a huge one in the transpiled output. See here what will come out:
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:
- Do you need reverse mapping? (If you don't know, then the answer is probably "No")
- If Yes, and your enum is not about strings, then you'll have to use a standard enum
- If No, continue at point 2
- Do you need to loop over enum members?
- If Yes, then use const assertion
- If No, continue at point 3
- 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.
Posted on December 22, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.