Union Types & Literal Types
Beginner · 18 min read · ▶ live playground · ✦ checkpoint
A union type is a TypeScript type that means one value may be one of several allowed types. A literal type is a type made from one exact value, so values like "idle" and "success" can model real choices instead of loose strings.
You already know the course's core idea: types are sets of allowed values. A union type joins sets with |, read as "or." string | number means "a string or a number."
One value, several possibilities
A union is still a promise. If you annotate a parameter as string | number, you promise callers may pass either kind of value:
The | does not mean "turn this into a new runtime thing." It is type-layer syntax. At runtime, webOrder is still a string, and counterOrder is still a number.
It also does not mean "both at once." One value of type string | number has one actual value at runtime. The type says the checker must allow either possibility before the program runs.
Literal types are exact values
A literal type's set contains one exact value, like "on" and nothing else. Join literal types with | and you get a literal union, a reusable list of allowed exact values:
This is where TypeScript starts paying for itself in small, daily ways. "dim" is a real string, but it is not part of the PowerMode set. The checker catches the typo before your code has a chance to send the wrong command.
A type alias is a name for a type you want to reuse. type PowerMode = "on" | "off"; is shorter and clearer than repeating "on" | "off" on every parameter and variable.
Why let widens and const stays narrow
TypeScript keeps exact facts when they will stay true. Widening is when the checker turns an exact literal type into a broader type so future assignments still make sense.
const defaultMode = "dark" can never be reassigned, so the checker can keep the exact type "dark". let savedMode = "dark" can be reassigned, so TypeScript widens it to string.
That widening is not a bug. It is the checker respecting your choice of let. If a variable should only hold the approved choices, give that variable the union promise:
type ColorMode = "dark" | "light";
let activeMode: ColorMode = "dark";
activeMode = "light";
console.log(activeMode);→ light
The pattern is const-first, inference-first, then annotate the boundary where your intent matters. A local constant can stay exact by inference. A mutable piece of app state often deserves an explicit literal union.
Model states with names, not guesses
A state is the current named condition of a process. Loading a profile is not "some string." It is one of a small set of real situations:
This is better than a plain string because the checker knows the full set. It is better than four separate booleans because impossible combinations disappear. You cannot be "loading" and "success" at the same time when one variable holds one literal union member.
Use this shape for tabs, payment status, notification level, feature flags, and step-by-step workflows. Real apps are full of one-of-these values.
One thing is still missing. Once a value is string | number, or once a state is one of four strings, how do you safely work with the specific member you have on a given line? That is the next lesson: narrowing, where the checker learns which possibility survived your code.
Checkpoint
Answer all three to mark this lesson complete