RizTech Academy logo
RizTech Academy
The Basic TypesLesson 6 of 620 min

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

  1. Declare let status: "paid" = "paid" and try to reassign it. Read the error.
  2. Write a type OrderStatus union of four strings, assign a valid value and a typo, and read the error.
  3. Compare const a = "paid" and let b = "paid" by hovering. Explain the different inferred types.
  4. Write a setStatus(status: OrderStatus) and call it with a const string (works) and a let string (fails). Fix the failing one two ways.
  5. Make an object const config = { theme: "dark" } and confirm config.theme is string. Add as const and confirm it is now "dark".
  6. Apply as const to an array and confirm it becomes a readonly tuple of literals.
  7. Build the ROLES array + type Role = typeof ROLES[number] pattern and confirm Role is the union. Note you now have one source of truth.

Official documentation

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