Literal Types
A literal type is a type whose only value is one specific constant — not “any string,” but exactly "GET"; not “any number,” but exactly 42. TypeScript supports string, number, boolean, and bigint literal types, and they become genuinely useful when combined into unions to model finite sets of allowed values. The catch is literal inference: TypeScript often widens a literal to its general type ("GET" becomes string) unless you tell it not to. This page covers declaring literal types, how widening works, and how as const and context keep literals narrow.
What a literal type is
A literal type restricts a value to a single constant. On its own a single literal is rarely useful, but it is the building block for literal unions.
let method: "GET" = "GET";
// ❌ Error: Type '"POST"' is not assignable to type '"GET"'
method = "POST";
type Bit = 0 | 1;
type Direction = "north" | "south" | "east" | "west";
const move = (dir: Direction) => console.log(dir);
move("north");
// ❌ Error: Argument of type '"up"' is not assignable to parameter of type 'Direction'
move("up");
A union of literals like Direction gives you an enum-like set of allowed values with zero runtime cost and full autocomplete — the idiomatic alternative to a TypeScript enum.
Literal widening
When you declare a variable with let, TypeScript widens the inferred literal to its base type, because a let binding is expected to change. A const binding cannot be reassigned, so its literal type is preserved.
let a = "hello"; // type: string (widened)
const b = "hello"; // type: "hello" (literal preserved)
let n = 10; // type: number
const m = 10; // type: 10
This is why const declarations give you precise literal types automatically, while let declarations broaden to string, number, or boolean. Understanding this distinction explains most surprising inference results.
Object properties widen too
Property values inside an object literal are widened to their base types, even when the object itself is const, because the object’s properties are mutable.
const config = { method: "GET", retries: 3 };
// config.method: string (not "GET")
// config.retries: number (not 3)
function request(opts: { method: "GET" | "POST" }) {}
// ❌ Error: Type 'string' is not assignable to type '"GET" | "POST"'
request(config);
Even though config is declared with const, config.method is string because you could later write config.method = "POST". Passing it where a literal union is expected fails. The fix is as const.
Locking in literals with as const
A const assertion (as const) tells TypeScript to infer the narrowest type and make everything deeply readonly. Properties keep their literal types, and arrays become readonly tuples of literals.
const config = { method: "GET", retries: 3 } as const;
// config.method: "GET", config.retries: 3, all readonly
function request(opts: { method: "GET" | "POST" }) {}
request(config); // ✅ now method is the literal "GET"
const routes = ["home", "about", "contact"] as const;
// type: readonly ["home", "about", "contact"]
type Route = (typeof routes)[number]; // "home" | "about" | "contact"
The (typeof routes)[number] pattern is a powerful idiom: derive a literal union type directly from a runtime array, keeping the values and the type in perfect sync.
Literal inference in context
You don’t always need as const. When a literal flows into a position that expects a literal type — a typed parameter or a typed variable — TypeScript keeps it narrow. This is called contextual typing.
type Align = "left" | "center" | "right";
// Inferred as Align because the parameter type drives inference
function setAlign(value: Align) {}
setAlign("center"); // "center" stays literal
// Annotating the variable keeps the literal
const align: Align = "left";
| Declaration | Inferred type |
|---|---|
let x = "a" | string (widened) |
const x = "a" | "a" (literal) |
const o = { k: "a" } | { k: string } |
const o = { k: "a" } as const | { readonly k: "a" } |
const arr = ["a", "b"] as const | readonly ["a", "b"] |
Prefer a union of string literals over a runtime
enumfor most finite sets. It has no emitted JavaScript, narrows cleanly, andas constplustypeof[number]lets you derive the type from a single source of values.
Best Practices
- Model finite option sets as unions of string literals instead of loose
string. - Remember
constpreserves literals whileletwidens them to the base type. - Reach for
as constwhen you need object properties or array elements to stay literal. - Derive unions from data with the
(typeof arr)[number]idiom to avoid duplication. - Annotate parameters and variables with literal-union types to get contextual narrowing for free.
- Prefer literal unions over
enumto ship zero extra runtime code. - Combine literal types with discriminated unions to drive exhaustive, type-safe branching.