Last 30 Days
No notifications
A literal type isn't string or number — it's the *exact value*: "loading", 42, true. Combined with unions and as const, literal types power autocomplete, exhaustive switches, and TypeScript's most magical feature: template literal types.
Most types describe a set of values. A literal type describes one value.
let s: "hello" = "hello";
// s = "world"; // ❌
const n: 42 = 42;
const b: true = true;On their own they're not very useful — but combined into unions, they become enums-without-runtime-cost.
type Direction = "up" "down" "left"
"right";
type HttpStatus = 200 404
500;
type Truthy = true;Autocomplete works in your editor for free.
By default TS *widens* literal types from variables (so you can reassign):
let s = "hello"; // string (widened)
const c = "hello"; // "hello" (preserved — const can't change)That's why const is the default for "give me the literal".
Object properties are also widened — even on const:
const config = { mode: "lazy" };
// config: { mode: string } — not { mode: "lazy" }as const — freeze itAdd as const to a literal expression and TS treats every property as readonly + literal:
const config = { mode: "lazy", retries: 3 } as const;
// { readonly mode: "lazy"; readonly retries: 3 }Same trick on arrays:
const methods = ["GET", "POST", "PUT"] as const;
// readonly ["GET", "POST", "PUT"]
type Method = typeof methods[number];
// "GET" "POST" "PUT"Annotation widening — as const vs explicit literal
Without as const, you can still narrow by annotating:
const dir: "up"
"down" = "up";Or by passing into a function whose parameter is a literal type — TS infers the literal at the call site (called *contextual typing*):
function move(d: "up"
"down") { /* … */ }
move("up"); // ✅ no wideningYou can build new string-literal types from existing ones using template-string syntax:
type Greeting = Hello, ${string};
const g1: Greeting = "Hello, Ada"; // ✅
const g2: Greeting = "Hi, Ada"; // ❌type Lang = "en" | "es";
type Bundle = "common" | "errors";
type I18nKey = ${Lang}.${Bundle};
// "en.common"
"en.errors" "es.common"
"es.errors"Combined with intrinsic helpers (Uppercase, Lowercase, Capitalize, Uncapitalize):
type EventName<T extends string> = on${Capitalize<T>};
type ClickHandler = EventName<"click">; // "onClick"Literal types are the building block for discriminated unions:
type Action =
| { type: "INC"; by: number }
| { type: "RESET" };function reduce(a: Action) {
switch (a.type) {
case "INC": return a.by;
case "RESET": return 0;
}
}
type DiceRoll = 1 2 3 4 5
6;
function nextRoll(): DiceRoll {
return (Math.floor(Math.random() * 6) + 1) as DiceRoll;
}function fetchJSON(url: string, method: "GET" | "POST" = "GET") {}
fetchJSON("/x", "GET"); // suggests GET / POSTconst ROLES = ["admin", "editor", "viewer"] as const;
type Role = typeof ROLES[number];function hasAccess(r: Role) { /* … */ }
type Route = "/" "/about"
"/users/:id";
function go(r: Route) { /* … */ }
go("/about"); // ✅Want a UserId that's a string but not interchangeable with any other string?
type UserId = string & { readonly __brand: unique symbol };
function makeUserId(s: string): UserId { return s as UserId; }function loadUser(id: UserId) {}
loadUser(makeUserId("u1")); // ✅
// loadUser("u1"); // ❌
string).