Video

Watch the full video on YouTube:

TypeScript enums and literal unions

Introduction

In this sixteenth chapter of our TypeScript series we focus on enums: a convenient way to restrict and centralize data to represent the multiple values or variants that our types can have.

We commonly use them to represent states. If we are building a game like Pokemon, enums can model battle status effects (electrified, burned, frozen, confused, asleep). If we are building an order system, they can model the order status (cancelled, shipped, delivered, received, completed).

We’ll also cover a popular alternative from the community: literal unions. In fact, many teams prefer not to use enums, and in this episode we’ll understand exactly why.

What Is an Enum

An enum is a way to restrict and centralize data: it declares a fixed set of values that a type can have, instead of allowing any string or number.

How to create an enum

We use the enum keyword and give it a name. Following the Pokemon example, we create the enums.ts file:

// Fixed set of statuses a Pokemon can have in battle
export enum PokemonStatus {
  Electrified,
  Burn,
  Freeze,
  Confuse,
  Sleep,
}

The notation inside the braces is very similar to JSON: first the name of each status.

Dot notation and indexed values

We use the enum through dot notation:

// Access each enum member with dot notation
PokemonStatus.Burn

Each member gets a value assigned automatically by index, unless we explicitly set one. In the enum above we have: Electrified = 0, Burn = 1, Freeze = 2, Confuse = 3 and Sleep = 4.

Custom Numeric Values

We can assign our own numeric value to a member. From that member down, each value increments from the previous one:

export enum PokemonStatus {
  Electrified = 6, // 6
  Burn, // 7
  Freeze, // 8
  Confuse, // 9
  Sleep, // 10
}

If we also force an intermediate value, only the members after it are affected:

export enum PokemonStatus {
  Electrified = 6, // 6
  Burn, // 7
  Freeze = 17, // 17
  Confuse, // 18
  Sleep, // 19
}

Burn still equals 7 because it comes before the jump. This is an important feature to keep in mind when working with enums.

Enums with Strings

Enum values don’t have to be numbers; we can also use strings. When we do, every member needs to be initialized:

export enum PokemonStatus {
  Electrified = "shock",
  Burn = "burn",
  Freeze = "freeze",
  Confuse = "confuse",
  Sleep = "sleep",
}

Values can differ from the member names (here Electrified has the value "shock"): no rule forces them to match. As a general rule, every member must be the same data type (all string or all number).

Using Enums in Functions

An enum is used like any other data type. We export a function that takes the status:

// Function that receives a status typed with the enum
export function applyStatus(status: PokemonStatus) {
  console.log(status)
}

And we use it in main.ts:

import { applyStatus, PokemonStatus } from "./enums"

applyStatus(PokemonStatus.Burn) // prints the status
applyStatus(PokemonStatus.Electrified) // prints another status

If we pass a value that doesn’t exist in the enum, TypeScript reports the error at compile time.

Key Feature: Enums Generate an Object in the Final JavaScript

Unlike other TypeScript types, enums do create an object (a JSON) in the final JavaScript after compilation. This is one of the reasons many teams prefer not to use them.

However, that same feature has an upside: being a JavaScript object, we can interact with the enum like any other object. For example, with Object.entries:

// Destructuring in the for-of argument
for (const [attribute, value] of Object.entries(PokemonStatus)) {
  console.log(attribute, value)
}

This is very useful on the frontend to populate a select, a table or any list, and on the backend to map user roles or access levels.

Reverse Mapping

If we run the loop with a numeric enum without custom values, we see the data twice: first the indexes with their value (0 → Electrified, 1 → Burn, …) and then each member with its index (Electrified → 0, Burn → 1, …). TypeScript first generates the object with the indexes and then a second version to map the indexes back to the members.

If we give the enum specific values (numbers or strings), the reverse mapping disappears and we only see each member with its value once.

The Downsides of Enums

Every feature has its downsides:

  1. You must import it wherever you need it.
  2. It generates an extra file/object in the JavaScript output, which can significantly impact your bundle.

Add both consequences together and it becomes clear why many developers simply prefer literal unions.

The Alternative: Literal Unions

In the previous episode we already saw type unions; now we create a literal union for the same statuses:

// Literal union: no object in the final JavaScript
export type PokemonStatusLiteral =
  | "electrified"
  | "burn"
  | "freeze"
  | "confuse"
  | "sleep"

It’s used in a very similar way to an enum:

export function applyStatusLiteral(status: PokemonStatusLiteral) {
  console.log(status)
}

applyStatusLiteral("confuse")

If we change to a value that doesn’t exist, TypeScript complains: the literal value doesn’t exist.

Important Difference

Even though we are using strings, those strings disappear at compile time and never reach the final JavaScript: they are only used as identifiers while we develop. The enum, on the other hand, does generate its final JSON.

When to Use One or the Other

It depends heavily on the use case:

Scenario Recommended option
You need the JSON available in the final JavaScript Enum
Your team comes from Java or C# where enums are very common Enum
The final output doesn’t impact your build Enum is fine
You don’t want an extra file or mandatory imports Literal union
You want the strings out of your bundle Literal union

In every other case, you’ll very commonly see developers prefer literal unions.

Best Practices

  1. Use enums to represent states (orders, roles, effects) and centralize those values in one place
  2. Initialize every member when you switch to string values, and keep them all the same data type
  3. Remember that enums generate an object in the final JavaScript: evaluate whether that impacts your bundle before choosing them
  4. Leverage Object.entries on an enum to iterate its members and populate selects, tables or lists
  5. Prefer literal unions when you don’t need the object at runtime: no extra file and their strings disappear after compilation
  6. Follow your team’s convention: if they come from Java or C#, enums will be the most natural option

Conclusion

In this episode we saw that enums are a convenient way to restrict and centralize data: how to declare them with the enum keyword, use dot notation, assign indexed or custom numeric values, initialize them with strings and use them as a function parameter.

We also understood their most debated feature: enums generate an object in the final JavaScript, which gives them the upside of being iterable with Object.entries but the downside of extra output. Against that, literal unions remove that cost and their strings disappear at compile time. The choice depends on the use case: if you need the JSON at runtime, use enums; if not, literal unions.

If you have any questions, leave them in the comments.

Repository

💻 Repository: https://github.com/radagva/ts-example

iOS Development Guide EP02: Variables, constants and data types