Enums and Literal Types
Fixed sets of allowed values, why a union of string literals is usually the better tool, and what an enum actually compiles into.
Fixed sets of allowed values, why a union of string literals is usually the better tool, and what an enum actually compiles into.
The unions lesson gave you a way to express a fixed set of
options: "pending" | "done" | "failed". There is also a
dedicated keyword for that job, called enum, and it appears in
a great deal of existing code.
It is worth knowing because you will read it, and worth understanding because it behaves unlike anything else here — it is the one construct that is not erased at compile time. By the end of this lesson you will know what it produces, where it genuinely helps, and why a union of string literals is usually the better tool.
type Status = "pending" | "uploading" | "done" | "failed";
function setStatus(status: Status): void { ... }
setStatus("done"); // fine
setStatus("Done"); // errorFour words. You get compile-time checking, autocomplete at the
call site, exhaustiveness in a switch, and — because it is
just text at runtime — values that serialise to JSON, arrive
from an API, and appear in a database exactly as written.
That last point matters more than it sounds. The string
"done" in your code, in your database and in a network
response are the same thing, with no conversion anywhere.
enum Status {
Pending,
Uploading,
Done,
Failed,
}
const current: Status = Status.Done;
console.log(current); // 2The values are numbers, assigned from zero. Status.Done is
2, which is the first surprise: what you store, log and send
is a number whose meaning lives only in your source.
You can assign the values:
enum Status {
Pending = "pending",
Uploading = "uploading",
Done = "done",
Failed = "failed",
}A string enum stores the text, which removes the worst problem. But it introduces its own, which the next section is about.
Unlike everything else in this language, an enum exists at runtime. It compiles to a real object:
var Status;
(function (Status) {
Status["Pending"] = "pending";
Status["Done"] = "done";
})(Status || (Status = {}));So it is both a type and a value. That is occasionally useful —
you can loop over Object.values(Status) — and it is why enums
are the one thing in this course that adds code to your output.
Bad — an enum at the boundary with stored data.
enum Status {
Done = "done",
}
type Upload = { status: Status };
const upload: Upload = JSON.parse(row); // status is "done"
if (upload.status === Status.Done) { ... } // works
processStatus("done");
// ~~~~~~
// Argument of type '"done"' is not assignable to 'Status'.Good — a union of literals.
type Status = "done" | "failed";
type Upload = { status: Status };
const upload: Upload = JSON.parse(row);
if (upload.status === "done") { ... }
processStatus("done"); // fineA string enum is nominal: the checker treats Status.Done
and the literal "done" as different types even though they are
the same characters at runtime. Everything else in this language
is structural, so this is a genuine exception, and it shows up
exactly where data crosses a boundary — every value from
JSON.parse, a database driver or a form has to be cast back
into the enum, and each cast is a check you are skipping.
A union has no such gap. The value from the database is the type.
Is the value, everywhere.
Structural, like the rest of the language. Text from a database, an API or a form matches the type with no cast.
Adds nothing at all to your compiled output.
A separate thing that happens to look the same.
A string enum is nominal: Status.Done and the literal
"done" are different types despite being the same
characters at runtime. Every value crossing a boundary needs
a cast, and each cast is a check you skipped.
Numeric enums also accept any number — const s: Status = 47
compiles. And const enum breaks isolatedModules, which
most bundlers require.
Two situations, and they are narrower than their popularity suggests.
You need the values at runtime. Iterating over every option to build a dropdown, validating input against the set:
Object.values(Status); // ["pending", "done", ...]A union cannot do this, because it does not exist at runtime. The workaround is one line and gives you both:
const STATUSES = ["pending", "uploading", "done", "failed"] as const;
type Status = (typeof STATUSES)[number]; // the union, derivedThat pattern is worth memorising — it is the standard answer to "I want a list and a type of its members" — so it is worth reading one piece at a time.
The array, written once
An ordinary array of strings. This is the thing you loop over to build a dropdown.
as const freezes it
Without it the type would widen to string[]. With it, the
type is the exact tuple ["pending", "uploading", ...].
(typeof STATUSES)[number] reads the elements
"The type of any element of that tuple" — which is the union of all four literals, derived rather than typed out again.
You are describing a protocol with numeric codes. A wire format or a C library with numbered states is a real fit, and naming the numbers is the whole point.
The mechanism is not limited to text:
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
type Answer = true | false; // the same as boolean
type Port = 80 | 443 | 8080;And they combine with other types, which is where the expressiveness shows:
type Size = "small" | "large" | number; // named sizes or exact
type Result = "loading" | Photo; // a state or the dataThat second one is a small discriminated union: check for the string and you have narrowed to the object.
UNION OF LITERALS - the default
type Status = "pending" | "done" | "failed";
checked, autocompleted, exhaustive in a switch
IS the string at runtime - no conversion at any boundary
adds no code to the output
ENUM
enum Status { Done = "done" }
exists at RUNTIME as a real object - the only thing here that does
numeric by default, from 0: what you store is a number
numeric enums accept ANY number, defeating the purpose
string enums are NOMINAL: "done" is not assignable to Status
-> every value from JSON, a database or a form needs a cast
const enum: inlines values, breaks isolatedModules. Avoid.
WHEN AN ENUM FITS
you need the values at runtime -> but see the pattern below
a wire protocol with numeric codes
THE PATTERN THAT GIVES YOU BOTH
const STATUSES = ["pending", "done", "failed"] as const;
type Status = (typeof STATUSES)[number];
one declaration -> the list AND the union
as const
freezes a literal so it is not widened to string/number
{ status: "done" } -> { status: string }
{ status: "done" } as const -> { readonly status: "done" }
the usual fix when a literal union "should" have matched
LITERALS ARE NOT ONLY STRINGS
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
type Result = "loading" | Photo; a small discriminated unionYou can now recognise an enum, know what it compiles to, and know the one-line pattern that gives you a runtime list and a compile-time union together. If you are starting something new, reach for the union; if you are reading existing code, you now know why every value from a database needed casting.
Next is Loops and Iteration, which is what makes collections
worth having. Everything so far has handled one value or used
map and filter; that lesson covers working through things
step by step, and the loop over object keys that hands you
something you did not expect.
Before you move on, take a fixed set of options in your code and
write it both ways — as an enum and with the as const pattern.
Then try passing a plain string to a function expecting each.
The difference in what the checker accepts is the whole argument
in about two minutes.