Literal Types & const Assertions

Reviewed & published by Brayan K

A literal type restricts a value to one exact constant — like the type "active" or 42 — and combined into unions they give you a precise, enum-like set of allowed values.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

A plain string type is like a blank text box — you can type anything. A literal union is like a dropdown menu: the field still holds a string, but only the specific options on the list are valid. "S" | "M" | "L" is the dropdown; "XXL" simply isn't on it, so the compiler rejects it before your program ever runs.

1. Literal Types: One Exact Value

A literal type is a type made of a single constant value. "active" as a type accepts only the string "active"; 6 as a type accepts only the number 6. By themselves they're rarely useful — their power shows when you join several with | to form a fixed set, like "north" | "south" | "east" | "west".

// One exact value as a type:
let mode: "dark" = "dark";
// mode = "light"; // ❌ Type '"light"' is not assignable to type '"dark"'

// The real power: a UNION of literal types = a fixed set of options:
type Direction = "north" | "south" | "east" | "west";
type Dice = 1 | 2 | 3 | 4 | 5 | 6;

function move(pos: [number, number], dir: Direction): [number, number] {
  // ... dir can ONLY be one of the four strings
  return pos;
}
// move(p, "up"); // ❌ '"up"' is not assignable to parameter of type 'Direction'
// A LITERAL TYPE is a type that is ONE exact value, not a whole category.
// "active" is a type with exactly one member: the string "active".
// In TypeScript:  let mode: "dark" = "dark";   // only "dark" is allowed
// At runtime these are ordinary strings/numbers — the constraint is in the types.

// A union of literals = a fixed menu of allowed values (very enum-like):
//   type Direction = "north" | "south" | "east" | "west";
const moves = ["north", "east", "east", "south"];

function move(position, direction) {
  switch (direction) {
    case "north": return [position[0], position[1] + 1];
    case "south": return [position[0], position[1] - 1];
    case "east":  return [position[0] + 1, position[1]];
    case "west":  return [position[0] - 1, position[1]];
    default:      return position;
  }
}

let pos = [0, 0];
for (const m of moves) {
  pos = move(pos, m);
}
console.log("final position:", pos); // final position: [ 2, 0 ]

// Numeric literals work too:
//   type Dice = 1 | 2 | 3 | 4 | 5 | 6;
function isWinningRoll(roll) {
  return roll === 6;
}
console.log(isWinningRoll(6)); // true
console.log(isWinningRoll(3)); // false

2. Widening vs Narrowing

When you initialise a variable, TypeScript decides between the broad type and the exact literal. A let can be reassigned, so TypeScript widens let theme = "dark" to string. A const never changes, so it narrows const fixed = "dark" to the literal type "dark". Understanding this explains a lot of "why is my type just string?" surprises.

let theme = "dark";    // type: string   (widened — reassignable)
const fixed = "dark";  // type: "dark"   (narrow literal — fixed)

function setMode(m: "dark" | "light") { /* ... */ }
setMode(fixed);  // ✅ "dark" fits
setMode(theme);  // ❌ Argument of type 'string' is not assignable to '"dark" | "light"'
// WIDENING: when TypeScript infers a GENERAL type instead of the literal.
// These are runtime-identical, but TypeScript types them differently:

let theme = "dark";    // inferred type: string  (WIDENED — let can be reassigned)
const fixed = "dark";  // inferred type: "dark"  (NARROW literal — const never changes)

// We can SEE the runtime values are the same:
console.log(theme, fixed);          // dark dark
console.log(theme === fixed);       // true

// Why it matters: a 'let' string can later hold any string, so TypeScript
// must widen it. A 'const' can never be reassigned, so it stays the exact literal.
theme = "light";                    // ✅ allowed for a string-typed let
console.log("theme is now:", theme); // theme is now: light

// In a .ts file:
//   let theme = "dark";   // type: string
//   const fixed = "dark"; // type: "dark"  (a literal type)

3. as const Assertions

An as const assertion tells TypeScript: "infer the narrowest possible type and make it readonly." Object fields keep their literal types instead of widening, arrays become readonly tuples of literals, and you can derive a union directly from data: typeof SIZES[number] turns ["sm","md","lg"] as const into "sm" | "md" | "lg" — one source of truth for both the values and the type.

const config = { role: "admin", level: 3 } as const;
// config.role  -> "admin" (not string), and readonly

const SIZES = ["sm", "md", "lg"] as const;
type Size = typeof SIZES[number]; // "sm" | "md" | "lg"

let s: Size = "md"; // ✅
// let bad: Size = "xl"; // ❌ '"xl"' is not assignable to type 'Size'
// 'as const' freezes a value into its NARROWEST, READONLY literal form.
// Without it, an object's fields widen to general types:
//   const a = { role: "admin" };          // role: string
//   const b = { role: "admin" } as const; // role: "admin" (readonly)

// At runtime, 'as const' makes the object deeply frozen-like (readonly in TS).
// Here we show the underlying data it describes:

const config = {
  role: "admin",
  level: 3,
  tags: ["beta", "internal"],
};
console.log(config.role);        // admin
console.log(config.level);       // 3
console.log(config.tags[0]);     // beta

// A super-common pattern: derive a union of literals FROM an array via 'as const'.
//   const SIZES = ["sm", "md", "lg"] as const;
//   type Size = typeof SIZES[number];   // "sm" | "md" | "lg"
const SIZES = ["sm", "md", "lg"];
console.log("allowed sizes:", SIZES.join(", ")); // allowed sizes: sm, md, lg
console.log("is 'md' allowed?", SIZES.includes("md")); // is 'md' allowed? true

// In a .ts file:
//   const config = { role: "admin", level: 3 } as const; // all fields readonly literals
//   const SIZES = ["sm", "md", "lg"] as const;
//   type Size = typeof SIZES[number]; // "sm" | "md" | "lg"

🎯 Your Turn

Model a traffic light whose colour is one of three literals. Fill in the two blanks marked ___ so the cycle goes red → green → amber → red, then run it.

// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// We're modelling a traffic light whose colour is one of three literals:
//   type Light = "red" | "amber" | "green";

function nextLight(light) {
  // Fill in the literal each colour should become:
  if (light === "red")   return ___;   // 👉 the string "green"
  if (light === "green") return "amber";
  if (light === "amber") return "red";
  return light;
}

console.log(nextLight("red"));   // green
console.log(nextLight("green")); // amber
console.log(nextLight(___));     // 👉 pass "amber" to get "red"

// ✅ Expected output:
//    green
//    amber
//    red

Common Errors (and the fix)

Pro Tips

Mini-Challenge: Door State Machine

No blanks this time — just a brief and a starting outline. Build the literal-typed state machine yourself, run it, and check your output against the example in the comments.

// 🎯 MINI-CHALLENGE: a tiny state machine with literal types
// A door is one of:  "locked" | "closed" | "open"
// Build transition(state, action) where action is "lock" | "unlock" | "push":
//
//   "locked" + "unlock" -> "closed"
//   "closed" + "lock"   -> "locked"
//   "closed" + "push"   -> "open"
//   "open"   + "push"   -> "closed"
//   anything else       -> return the SAME state unchanged
//
// Then run these and log each result:
//   transition("locked", "unlock")  -> "closed"
//   transition("closed", "push")    -> "open"
//   transition("open", "push")      -> "closed"
//
// ✅ Expected output:
//    closed
//    open
//    closed

// your code here

🎉 Lesson Complete

Practice quiz

What is a literal type?

  • A type that accepts any string
  • A runtime literal value
  • A type whose only value is one exact constant
  • A union of all numbers

Answer: A type whose only value is one exact constant. A literal type like "active" or 42 accepts only that one exact value.

What does a union of literals like "sm" | "md" | "lg" give you?

  • A precise, enum-like set of allowed values
  • A random value
  • Any string
  • A tuple

Answer: A precise, enum-like set of allowed values. Joining literals with | creates a fixed set of allowed options, like a dropdown menu.

What type does let theme = "dark" infer?

  • "dark"
  • any
  • literal
  • string

Answer: string. A let can be reassigned, so TypeScript widens it to string.

What type does const fixed = "dark" infer?

  • string
  • "dark"
  • any
  • char

Answer: "dark". A const never changes, so it narrows to the literal type "dark".

What does as const do to a value?

  • Freezes it into its narrowest readonly literal form
  • Makes it mutable
  • Converts it to a string
  • Removes its type

Answer: Freezes it into its narrowest readonly literal form. as const infers the narrowest possible type and makes the value readonly.

For const SIZES = ["sm", "md", "lg"] as const, what is typeof SIZES[number]?

  • string[]
  • string
  • "sm" | "md" | "lg"
  • never

Answer: "sm" | "md" | "lg". typeof ARR[number] on an as const array yields the union of its element literals.

Why might passing a let string to a function expecting "dark" | "light" fail?

  • The value is wrong
  • The let was widened to string, which is not assignable to the literal union
  • Functions cannot take strings
  • let is not allowed in TypeScript

Answer: The let was widened to string, which is not assignable to the literal union. Even with a correct value, a widened string is not assignable to the narrower literal union.

Which is a valid numeric literal union type?

  • type Dice = number(6)
  • type Dice = int<6>
  • type Dice = [1,6]
  • type Dice = 1 | 2 | 3 | 4 | 5 | 6

Answer: type Dice = 1 | 2 | 3 | 4 | 5 | 6. Numeric literals combine with | just like strings: 1 | 2 | 3 | 4 | 5 | 6.

Compared with an enum, a literal union of strings is generally...

  • Heavier and needs runtime code
  • Lighter, needs no runtime object, and is type-safe
  • Unsafe
  • Only for numbers

Answer: Lighter, needs no runtime object, and is type-safe. A literal union is lighter and needs no runtime object; most teams prefer it for string sets.

Why declare options as const OPTS = [...] as const and derive the type with typeof OPTS[number]?

  • It runs faster
  • It disables type checking
  • It keeps a single source of truth so the array and type stay in sync
  • It is required syntax

Answer: It keeps a single source of truth so the array and type stay in sync. Deriving the type from the array means changing the array updates the type automatically.

Continue this course

Frequently asked questions

What exactly is a literal type?

It is a type whose only allowed value is one specific literal — for example the type "active" accepts only the string "active", and the type 42 accepts only the number 42. On their own they're niche, but combined with | into a union (like "sm" | "md" | "lg") they describe a precise, finite set of allowed values.

What is the difference between widening and narrowing?

Widening is when TypeScript infers a general type (let x = "dark" gives type string), because a let can be reassigned. Narrowing keeps the exact literal (const x = "dark" gives type "dark"), because a const can never change. as const forces the narrow, literal form even on objects and arrays.

When should I use 'as const'?

Use as const when you want a value's literal types preserved and the value treated as readonly — typically for configuration objects, lookup tables, and arrays you want to turn into a union of literals via typeof SIZES[number]. It prevents accidental widening and accidental mutation.

Literal union vs enum — which should I pick?

A union of string literals (type Size = "sm" | "md" | "lg") is lighter, needs no runtime object, and is fully type-safe — most teams prefer it. Reach for an enum when you want a named runtime value, reverse mapping, or auto-incrementing numbers. You'll meet enums in the next lesson.