AcademyTypeScript
Literal types, unions and as const
Why a string in a variable stops being a literal, what as const freezes, and why a union of strings beats an enum almost every time.
3 min readUpdated 3 exercises
A value can be its own type
'idle' is a string. It is also a type — the type whose only value is 'idle'. Union a few of those and you have described a state machine:
type Status = 'idle' | 'loading' | 'done' | 'failed';
let status: Status = 'idle';
status = 'loadingg'; // Error, and the typo is caught at the assignmentThis is the workhorse of TypeScript. It costs nothing at runtime — these are plain strings — and it gives autocomplete, exhaustiveness and a compile error on every typo.
Widening: why your literal turned back into a string
TypeScript widens a literal to its base type whenever the value could change:
const a = 'idle'; // type is 'idle' — const, so it cannot change
let b = 'idle'; // type is string — it could be reassigned
const config = { status: 'idle' };
config.status; // type is string — the property is mutableThe third one is the surprise. const protects the binding, not the properties, so every field of an object literal widens. Which is why this fails:
function setStatus(status: Status) { /* ... */ }
const config = { status: 'idle' };
setStatus(config.status);
// Error: Argument of type 'string' is not assignable to parameter of type 'Status'.Nothing is wrong with your value. The type just forgot what it was.
as const remembers
as const tells the compiler to keep the narrowest type it can, and to mark everything readonly:
const config = { status: 'idle' } as const;
// { readonly status: 'idle' }
setStatus(config.status); // fine
const ROLES = ['admin', 'editor', 'viewer'] as const;
// readonly ['admin', 'editor', 'viewer']
type Role = (typeof ROLES)[number];
// 'admin' | 'editor' | 'viewer'That last pair of lines is worth memorising. One array, written once, gives you both the runtime list to iterate and the union to type against — and they cannot drift apart, because the second is derived from the first.
Unions instead of enums
enum is one of the few TypeScript features that emits runtime code, and the object it emits behaves in ways nothing else in the language does.
enum Status { Idle, Loading }
// Numeric enums are bidirectional at runtime: Status[0] === 'Idle'.
// And this compiles, in older configurations:
const s: Status = 42;A union of literals plus as const gives you the same ergonomics with none of that:
enum | union + as const | |
|---|---|---|
| Runtime output | an object | nothing |
| Value in the debugger | 0 | 'idle' |
| Value in the database | needs mapping | the string itself |
| Exhaustiveness | works | works |
import type erasable | no | yes |
const enum avoids the runtime object but breaks under isolatedModules, which most bundlers now require. The union is the option that keeps working everywhere.
Where unions get uncomfortable
A union of objects without a common discriminant is hard to work with — you cannot read a property that does not exist on every member, so you end up with in checks everywhere. The fix is not to avoid unions, it is to add the discriminant:
// Hard to narrow
type Response = { data: string } | { error: string };
// One comparison narrows it, and a switch can be proved exhaustive
type Response =
| { status: 'ok'; data: string }
| { status: 'error'; error: string };The second version is also the one that survives a third case being added next quarter.
Now practice it
2 challenges · 1 quizReading this page is the cheap half. These are the exercises that make you use it.
- Union & Literal Types — Direction & StatusBuilds the direction and status unions this page describes, and shows what the compiler does with a typo.ChallengeSolve · EasyChallengeSolve · Easy
- as const — Enum-like String UnionsProThe as const array plus (typeof x)[number] pattern, which replaces an enum without emitting anything.ChallengeSolve · EasyChallengeSolve · Easy
- Fundamentals · TypeScript IntermediateIntermediate questions where widening is usually the reason an answer that looks right is not.QuizTake · MediumQuizTake · Medium
Read next
Narrowing: how a union becomes one type
The checks TypeScript understands — typeof, instanceof, in, truthiness, discriminants — and what makes a narrowing silently fall apart again.
3 min
Type assertions: as vs satisfies
What a type assertion actually does to your program, the two places it is defensible, and why satisfies gives you the check without losing the type.
3 min