Enums
Overview
TL;DR Enums present with tricky functionality under the hood, and don't bring the same value that they do in other languages (C#, C++, Java, etc.).
You are almost always better off with constant objects:
export const Status = {Active: "ACTIVE",Inactive: "INACTIVE",Pending: "PENDING",} as constexport type Status = (typeof Status)[keyof typeof Status]
Read more to learn about the nuances of enums in TypeScript.
The Long Answer
Enums represent both values and types in TS.
- Type Side: Enums introduce a new type. For example,
enum Color { Red, Green, Blue }
creates a type Color that can be used to type variables. - Value Side: Enums generate an object at runtime, which contains the enum members as properties. This means enums have both compile-time type information and runtime representations.
They contain unique characteristics that cannot easily be compiled down to plain JavaScript. They must first be transformed, which makes them incompatible with Node's experimental TypeScript support.
To delve deeper, let's consider the two type systems available in TS.
Structural Typing
Definition: Also known as "duck typing," structural typing bases type compatibility on the shape or structure of the types. If two types have the same structure, they are considered compatible, regardless of their explicit declarations or origins.
Analogy: "If it walks like a duck and quacks like a duck, it's a duck."
Examples:
- Interfaces and Type Aliases: Types defined by their properties.
- Functions and Objects: Compatibility based on parameter and return types.
Nominal Typing
Definition: Type compatibility is based on explicit declarations and names. Two types are compatible only if they share the same name or explicitly declare a relationship (like inheritance).
Analogy: "If it's named a duck, it's a duck."
Examples:
- Classes with Specific Names: Instances must be of the declared class type.
TypeScript's Structural Type System
TypeScript predominantly employs structural typing, emphasizing type compatibility based on shared structures rather than explicit declarations. This approach offers several advantages:
- Flexibility: Developers can use objects and functions interchangeably as long as their structures align.
- Ease of Integration: Simplifies compatibility between different modules and libraries.
- Less Boilerplate: Reduces the need for explicit type declarations and casting.
example:
interface Point {x: numbery: number}function logPoint(p: Point) {console.log(`Point at (${p.x}, ${p.y})`)}const point = {x: 10, y: 20, z: 30}logPoint(point) // Valid: 'point' has at least 'x' and 'y'
In the above example, even though point has an extra property z, it's compatible with the Point interface because it includes the required properties.
How Enums Interact with Structural Typing
While TypeScript is structurally typed, enums introduce aspects of nominal typing, creating a unique interplay between the two paradigms. Here's how enums fit—or sometimes don't fit—into TypeScript's structural type system:
Dual Nature of Enums
- Type Side: Enums define a distinct type, e.g., Direction or Status.
- Value Side: They generate JavaScript objects with their members.
Numeric Enums
Numeric enums exhibit a mix of structural and nominal typing characteristics:
enum Status {Active,Inactive,}enum Direction {Up,Down,}let s: Status = Status.Activelet d: Direction = Direction.Ups = d // Valid, both are enums with numeric values
Explanation: Since both Status and Direction are numeric enums (backed by numbers), TypeScript considers them compatible because they're structurally similar (both are numbers). This behavior leans towards structural typing.
Potential Issues:
let status: Status = 1 // Valid, even if 1 isn't a defined member
Numeric enums allow any number, reducing type safety and emphasizing structural compatibility over strict nominal typing.
String Enums
String enums are not compatible even if the string values match because the enum types are distinct. This behavior emulates nominal typing.
enum Status {Active = "ACTIVE",Inactive = "INACTIVE",}let currentStatus: Status = "ACTIVE" // Compile-time errorcurrentStatus = Status.Active // Valid
In the above example, assigning a raw string to a string enum type results in a compile-time error, reinforcing nominal boundaries.
Implications and Best Practices
Given the hybrid nature of enums in TypeScript, developers should consider alternative patterns that align more consistently with structural typing:
type Status = "ACTIVE" | "INACTIVE" | "PENDING"let currentStatus: Status = "ACTIVE" // ValidcurrentStatus = "INACTIVE" // ValidcurrentStatus = "UNKNOWN" // Compile-time error
as const
with objects
const Status = {Active: "ACTIVE",Inactive: "INACTIVE",Pending: "PENDING",} as consttype Status = (typeof Status)[keyof typeof Status]let currentStatus: Status = Status.Active // ValidcurrentStatus = "INACTIVE" // ValidcurrentStatus = "UNKNOWN" // Compile-time error
Advantages
- Immutability: Using as const ensures the object properties are read-only.
- Type Inference: Automatically infers literal types for properties.
- No Extra JavaScript: Unlike enums, this pattern doesn't generate additional runtime code (unless used explicitly).
Summary
- Type Systems:
- Structural Typing: Focuses on the shape of types.
- Nominal Typing: Focuses on explicit declarations and names.
- TypeScript: Primarily uses structural typing but incorporates nominal aspects through constructs like enums.
- Enums:
- Numeric Enums: More aligned with structural typing but can compromise type safety.
- String Enums: Introduce nominal characteristics, enhancing type safety, but clash with TypeScript's predominantly structural type system.
- Dual Nature: Serve both as types and runtime values, bridging compile-time and runtime.
- Best Practices:
- Evaluate Necessity: Use enums when their features provide tangible benefits.
- Favor Alternatives for Type Safety and Simplicity: Prefer union types or as const objects when appropriate.
- Be Mindful of Runtime Overhead: Consider the impact on bundle size and performance, especially in large-scale applications.