Understanding TypeScript Enums and Their Alternatives
Written on
TypeScript enums, or enumerated types, represent a unique data structure that can encapsulate a set of constants. While enums are not exclusive to TypeScript, their compilation into JavaScript offers a compelling reason to examine their utility in this language.
In this concise and practical guide, we will investigate the compilation of enums to JavaScript, the potential inconsistencies in the resulting code, ways to utilize them more effectively, and an alternative approach to enums.
Let’s dive in:
TypeScript Enums
To define an enum in TypeScript, you can use the following syntax:
enum Protocol {
HTTP,
HTTPS,
WS
}
In this case, TypeScript automatically assigns numeric values to these constants, starting at 0.
A key advantage of enums is their dual role as types, allowing us to enforce type safety during compilation while retaining type information at runtime. This contrasts with conventional TypeScript types, which are removed during execution.
We can leverage enums as types and build logic around their values, as demonstrated below:
function printProtocol(protocol: Protocol): void {
switch (protocol) {
case Protocol.HTTP:
console.log("web insecure");
break;
case Protocol.HTTPS:
console.log("web secure");
break;
case Protocol.WS:
console.log("socket protocol");
break;
default:
console.log("Unknown protocol");}
}
printProtocol(Protocol.HTTP);
The Challenge
JavaScript does not inherently support enums. Although there is a TC39 proposal in the works, as of this writing, enums have not yet been integrated into the core language.
Consequently, when TypeScript code is compiled into JavaScript, enums are transformed into bidirectionally accessible objects as follows:
{
0: "HTTP",
1: "HTTPS",
2: "WS",
HTTP: 0,
HTTPS: 1,
WS: 2
}
While this structure provides access from multiple points, it leads to redundancy and potential confusion. The complexity increases further when we assign string values to an enum:
Enums with String Values
Let’s assign string values to our previous enum example:
enum Protocol {
HTTP = "http",
HTTPS = "https",
WS = "websocket"
}
This compiles into a cleaner JavaScript object:
{
HTTP: "http",
HTTPS: "https",
WS: "websocket"
}
While this representation is clearer, it highlights the variance in compiled output based on the value types assigned to the enum constituents. Depending on your specific needs, this inconsistency can present challenges.
An Alternative Approach: The Const Assertion
The as const keyword in TypeScript, known as const assertion, provides a reliable alternative to enums by producing consistent compiled JavaScript, regardless of the value types assigned to the constants.
This can be implemented as follows:
const Protocol = {
HTTP: 0,
HTTPS: 1,
WS: 2
} as const;
Since this is treated as a constant, the bidirectional object structure is eliminated, resulting in consistent output irrespective of whether values are numeric or string-based.
The compiled JavaScript will yield:
{
HTTP: 0,
HTTPS: 1,
WS: 2
}
Deriving Types from Const Assertions
While this approach is advantageous, it limits our ability to use Protocol as a type in its current form. Attempting to do so will result in a compiler error:
'Protocol' refers to a value, but is being used as a type here.
Did you mean 'typeof Protocol'?
Fortunately, TypeScript provides a way to navigate around this limitation. A const assertion also narrows the type of the created object, enabling us to derive specific types as follows:
const Protocol = {
HTTP: 0,
HTTPS: 1,
WS: 2
} as const;
type Protocol = keyof typeof Protocol;
type ProtocolValues = typeof Protocol[keyof typeof Protocol];
function printProtocol(protocol: ProtocolValues): void {
switch (protocol) {
case Protocol.HTTP:
console.log("web insecure");
break;
case Protocol.HTTPS:
console.log("web secure");
break;
case Protocol.WS:
console.log("socket protocol");
break;
default:
console.log("Unknown protocol");}
}
printProtocol(Protocol.HTTP);
The Protocol type can now take on values of "HTTP" | "HTTPS" | "WS", while the ProtocolValues type can be 0 | 1 | 2.
This allows for precise type definitions while maintaining runtime logic consistency. Additionally, we can use string values in our constants:
const Protocol = {
HTTP: "http",
HTTPS: "https",
WS: "websocket"
} as const;
This would update our ProtocolValues to "http" | "https" | "websocket".
Another interesting aspect is that mixed data types can now be assigned to our constants:
const Protocol = {
HTTP: "http",
HTTPS: 2,
WS: "websocket"
} as const;
This would adjust our ProtocolValues type to the narrowed type of "http" | 2 | "websocket".
Video Demo
Conclusion
In this article, we explored TypeScript enums, their compilation to JavaScript, and an alternative known as const assertion. This method not only simplifies refactoring—allowing updates in one location to automatically adjust type inference—but also preserves the benefits of enums without the inconsistencies in compiled code.
If you are passionate about software engineering and keen on discovering alternative strategies for effective software design and development, consider following me for more insightful and practical technical content.
Thank you for reading, and I look forward to sharing more in my next article!
Cheers :)