Literal Types & Const Assertions

intermediate literal-types as-const widening

A literal type is a type that has exactly one possible value. Instead of string, it could be the literal "left". Instead of number, the literal 42. Combine them with unions and we get the safest, most expressive APIs in TS.

What’s a literal type?

let dir: "left" | "right" | "up" | "down";
dir = "left";   // ✅
dir = "north";  // ❌ Type '"north"' is not assignable

In simple language, we’re saying “this variable can only hold one of these exact strings”. It’s a closed set — and TS will flag any typo.

The widening problem

Here’s the catch. const of a literal keeps the literal type, but let widens to the primitive.

const a = "left";  // type: "left" (literal)
let b = "left";    // type: string  (widened)

const dir: "left" | "right" = a; // ✅
const dir2: "left" | "right" = b; // ❌ string not assignable

Same thing happens with object properties — they widen by default.

const config = { mode: "dark" }; // mode is string, not "dark"

type Theme = { mode: "light" | "dark" };
const t: Theme = config; // ❌

This is where as const saves us.

as const — the const assertion

Adding as const tells TS: “treat this exactly as written — don’t widen anything, make every property readonly, treat strings/numbers as literals”.

const config = { mode: "dark" } as const;
// type: { readonly mode: "dark" }

const t: Theme = config; // ✅

Deriving types from data

This is the killer use case. Define data once, derive types from it.

const STATUSES = ["pending", "approved", "rejected"] as const;
// type: readonly ["pending", "approved", "rejected"]

type Status = typeof STATUSES[number];
// type: "pending" | "approved" | "rejected"

We just got a string-literal union from an array, without duplicating values. Add a new status to the array and the type updates automatically.

const ROUTES = {
  home: "/",
  profile: "/profile",
  settings: "/settings",
} as const;

type RoutePath = typeof ROUTES[keyof typeof ROUTES];
// type: "/" | "/profile" | "/settings"

Numeric and boolean literals

Not just strings — numbers and booleans can be literals too.

type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
type Coin = "heads" | "tails";
type Toggle = true | false; // same as boolean

function roll(): DiceRoll {
  return (Math.floor(Math.random() * 6) + 1) as DiceRoll;
}

Literal types as function returns

A function returning a literal lets callers branch precisely.

function getEnv(): "dev" | "prod" {
  return process.env.NODE_ENV === "production" ? "prod" : "dev";
}

Combining with discriminated unions

Literals on a shared kind field are how discriminated unions work — see the next note.

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number };

Common gotcha: passing object literals to typed slots

type Btn = { variant: "primary" | "secondary" };

const a = { variant: "primary" };
const btn: Btn = a; // ❌ a.variant is string, not "primary"

const c = { variant: "primary" } as const;
const btn2: Btn = c; // ✅

Interview soundbite

“A literal type is a type with one possible value, like the string 'left' rather than all of string. Combined with unions they make tight, typo-proof APIs. as const stops widening — keeps literals literal and makes objects/arrays deeply readonly. Together they let us derive types from runtime data.”