Skip to content

Literals & Widening

A literal type is a type whose only value is a single specific literal. "GET" is a type; the only value assignable to it is the string "GET". Same for 42, true, and so on.

let method: "GET" = "GET";
method = "POST"; // ❌ Type '"POST"' is not assignable to type '"GET"'.

On their own, literal types are rarely useful. Their power comes from unions of literals, which model “one of these exact values”:

type Method = "GET" | "POST" | "PUT" | "DELETE";
type Dice = 1 | 2 | 3 | 4 | 5 | 6;

This is how you replace loose string parameters with precise, self-documenting, autocompleting ones.

TypeScript widens a literal to its general type in places where the value could change. The rule you met in Foundations: const keeps the literal, let widens.

const a = "GET"; // type: "GET"
let b = "GET"; // type: string (widened — b is reassignable)

The same widening happens to object properties, because object properties are mutable by default:

const config = { method: "GET" };
// type: { method: string } — NOT { method: "GET" }
function send(m: "GET" | "POST") {}
send(config.method); // ❌ string is not assignable to "GET" | "POST"

Even though config is a const, its method property can be reassigned (config.method = "POST" is legal), so the compiler widens it to string. This surprises everyone once.

A const assertion (as const) tells the compiler “treat this value as deeply immutable, and keep every literal narrow.”

const config = { method: "GET" } as const;
// type: { readonly method: "GET" }
send(config.method); // ✅ now "GET" survives

as const does three things at once:

  1. Literals stay literal"GET" instead of string.
  2. Properties become readonly — reflecting the immutability you promised.
  3. Arrays become readonly tuples[1, 2] becomes readonly [1, 2], not number[].
flowchart TB
  val["value: { method: 'GET' }"] -->|default| wide["type: { method: string }
(widened, mutable)"]
  val -->|as const| narrow["type: { readonly method: 'GET' }
(literal, readonly)"]
Widening vs as const

as const shines for lookup tables and configuration where you want the exact keys and values in the type:

const ROUTES = {
home: "/",
profile: "/profile",
settings: "/settings",
} as const;
type Route = typeof ROUTES[keyof typeof ROUTES];
// type: "/" | "/profile" | "/settings"

Without as const, ROUTES would be { home: string; profile: string; settings: string } and you’d lose every specific path. With it, you can derive precise union types straight from the data — a pattern you’ll see constantly in the type-level module.

What is the type of `const x = "GET"` versus `let y = "GET"`?
Why is `method` typed as string in `const config = { method: "GET" }`?
What does `as const` do?
Why is a union of literals like "GET" | "POST" better than string for a parameter?