Literal types and as const
A literal type is a type that is one specific value — not "any string" but exactly "paid"; not
"any number" but exactly 200. It sounds almost too small to be a type, and yet literal types are one
of the things that make TypeScript genuinely powerful, because combined with unions they let you say
"this must be one of these exact values". This lesson is literal types and the as const tool that
unlocks them.
A type that is one value
let status: "paid" = "paid";
status = "pending"; // error: Type '"pending"' is not assignable to type '"paid"'
"paid" is a type — the type whose only value is the string "paid". A variable of that type can hold
"paid" and nothing else. On its own that is not very useful (a variable that can only ever be one
value). It becomes powerful the moment you combine literals with a union:
type OrderStatus = "pending" | "paid" | "shipped" | "delivered";
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
type Alignment = "left" | "center" | "right";
let status: OrderStatus = "paid"; // fine
status = "pade"; // error: not one of the four
Now OrderStatus means "exactly one of these four strings" — the pattern you met in the enums lesson,
and the one most TypeScript code uses for a fixed set of options. Literal unions give you the safety of
an enum (typos rejected, only valid values allowed) as a plain, erased type. They are the workhorse use
of literal types, and you will write them constantly.
Where literals come from — const narrows
You met this in the inference lesson: const infers the literal type, let infers the wide type.
const a = "paid"; // type: "paid" (the literal)
let b = "paid"; // type: string (the wide type)
Because const a can never change, TypeScript infers its exact type "paid". This matters when you
pass values to functions that expect a literal union:
function setStatus(status: OrderStatus) { /* ... */ }
const s = "paid"; // type "paid" — fits OrderStatus
setStatus(s); // fine
let m = "paid"; // type string — does NOT fit OrderStatus
setStatus(m); // error: Argument of type 'string' is not assignable to 'OrderStatus'
setStatus(m) fails because m is inferred as the wide string, and a string might be any string,
not necessarily one of the four. This is a genuinely common early confusion — "but it is "paid"!" —
and the fix is either const or an annotation. Understanding it is why the let/const inference
difference mattered.
The object problem, and as const
The const narrowing only reaches the top level. Inside an object, properties are still inferred
wide, because object properties are mutable:
const config = { theme: "dark", retries: 3 };
// inferred: { theme: string; retries: number } — theme is string, not "dark"!
setTheme(config.theme); // error if setTheme wants "light" | "dark" — config.theme is string
Even though config is const, config.theme is inferred as string (not "dark"), because you
could reassign config.theme = "blue" later. To get literal types inside an object, use as const:
const config = { theme: "dark", retries: 3 } as const;
// inferred: { readonly theme: "dark"; readonly retries: 3 }
as const does two things: it makes every property readonly (deeply — nested objects and arrays
too), and it infers the narrowest type for each value — "dark" instead of string, 3 instead of
number. Now config.theme has type "dark" and fits a "light" | "dark" parameter. as const is
the tool for "this is a fixed, immutable set of exact values", and you will use it for configuration,
constant lookup tables, and anywhere you want literal types to survive inside a structure.
as const on arrays — a readonly tuple of literals
as const on an array is especially useful — it turns a plain array into a readonly tuple of exact
literals:
const roles = ["admin", "editor", "viewer"];
// inferred: string[]
const roles2 = ["admin", "editor", "viewer"] as const;
// inferred: readonly ["admin", "editor", "viewer"]
This is the bridge between a runtime array and a literal-union type, and it is a common, elegant
pattern: define the values once as a const array, and derive the union type from it (using typeof
and indexed access, which the advanced-types module covers):
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = typeof ROLES[number]; // "admin" | "editor" | "viewer"
Now you have one source of truth: ROLES to iterate over at runtime, and Role as the type — and
they cannot drift apart, because the type is derived from the array. This solves the enum lesson's "but
I need to loop over the values" problem cleanly, with no runtime enum object.
Why literal types matter
Literal types are what let you make invalid states impossible. Instead of status: string (any string,
including nonsense), you write status: "pending" | "paid" | "shipped" | "delivered" and the compiler
rejects everything else. Instead of a magic string "large" passed to a function that quietly does the
wrong thing on a typo, the typo is a compile error. This is the beginning of the course's recurring
theme — modelling your data so that illegal values cannot be represented (a whole best-practices
lesson) — and literal types are the simplest, most-used tool for it. Every time you replace a bare
string with a union of the specific strings it can actually be, you make a class of bug impossible.
Check your work
What a literal type is. A type that is one specific value — "paid", 200 — not the whole
string or number.
Where literal types become powerful. Combined with unions — "pending" | "paid" | ... — a fixed
set of exact values, the workhorse pattern.
How const and let differ for literals. const infers the exact literal ("paid"); let
infers the wide type (string).
Why setStatus(m) can fail where setStatus(s) succeeds. let m is string (too wide); const s is "paid" (fits the union).
Why object properties are inferred wide. They are mutable, so const config still gives
config.theme the type string.
What as const does. Makes properties deeply readonly and infers the narrowest literal type for
each value.
What as const does to an array. Turns it into a readonly tuple of exact literals — the bridge
to a literal-union type.
The one-source-of-truth pattern. A const array as const plus typeof arr[number] gives a
runtime array and a matching union type that cannot drift.
Practice
- Declare
let status: "paid" = "paid"and try to reassign it. Read the error. - Write a
type OrderStatusunion of four strings, assign a valid value and a typo, and read the error. - Compare
const a = "paid"andlet b = "paid"by hovering. Explain the different inferred types. - Write a
setStatus(status: OrderStatus)and call it with aconststring (works) and aletstring (fails). Fix the failing one two ways. - Make an object
const config = { theme: "dark" }and confirmconfig.themeisstring. Addas constand confirm it is now"dark". - Apply
as constto an array and confirm it becomes areadonlytuple of literals. - Build the
ROLESarray +type Role = typeof ROLES[number]pattern and confirmRoleis the union. Note you now have one source of truth.
Official documentation
- TypeScript — Literal Types — Literals, and literal unions.
- TypeScript — const assertions (
as const) — Deep readonly and literal inference. - TypeScript — Literal inference — Why
constnarrows and objects do not.
Next module — Objects, Interfaces and Type Aliases: describing the shape of your data.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship