Literal types in TypeScript
Literal types in TypeScript pin a variable to exact values - like "up" or 404 - instead of broad categories like string or number.
Theory
TL;DR
- Literal types are like labeled parking spots: your variable fits only
"up", not anystring - Main difference:
stringaccepts"banana";"up" | "down"rejects it at compile time - Use when you have 3-10 fixed options known at dev time
- TypeScript widens literals in loose contexts (arrays, plain objects) unless you use
as const
Quick example
// Without literal - accepts any string
type Status = string;
function setStatus(s: Status) {}
setStatus("banana"); // No error
// With literal - only exact values
type Status = "loading" | "success" | "error";
function setStatus(s: Status) {}
setStatus("banana"); // Error: '"banana"' is not assignable to type 'Status'
setStatus("success"); // OKThe compiler checks the exact string token, not just the type category. Bad values are blocked before the code runs.
Key difference
A plain string accepts any sequence of characters. "loading" | "success" | "error" accepts only those three tokens. You trade flexibility for a guarantee. TypeScript matches the exact value during type checking and erases everything at compile time, so the JavaScript output sees plain strings. No runtime cost.
One thing that trips up most developers the first time: TypeScript widens literals automatically in some contexts. Write { dir: "up" } and TypeScript infers { dir: string }, not { dir: "up" }. The literal gets lost. To keep it, use as const.
When to use
- UI states:
type Status = "draft" | "published" | "archived" - HTTP status codes:
type Code = 200 | 201 | 404 - Alignment and config keys:
type Align = "left" | "center" | "right" - Skip for user input (email, name) - use
string - Skip for boolean flags - use
boolean, unless you specifically needtrueonly
as const: keeping literals from widening
// Without as const - TypeScript widens to string[]
const directions = ["up", "down"];
type Dir = typeof directions[0]; // string, not "up"
// With as const - stays readonly ["up", "down"]
const directions = ["up", "down"] as const;
type Dir = typeof directions[number]; // "up" | "down"This matters whenever you derive types from runtime arrays or config objects.
How the compiler handles this
TypeScript treats literal types as branded primitives during type checking. It matches the exact token "up" against union members. At compile time, literals become plain JS values - Node.js and the browser see only raw strings or numbers. No overhead. Widening happens in covariance contexts (array literals, return types) unless as const freezes the inference.
Common mistakes
Forgetting as const on arrays
const roles = ["admin", "user"]; // infers string[]
type Role = typeof roles[number]; // string, not "admin" | "user"
// Fix:
const roles = ["admin", "user"] as const;
type Role = typeof roles[number]; // "admin" | "user"Assuming literals enforce values at runtime
function validate(code: 200 | 404) { return true; }
// TypeScript erases types on compile.
// JS sees: function validate(code) { return true; }
// Fix: pair with Zod: z.union([z.literal(200), z.literal(404)])Widening the return type by accident
// Returns string, not "red" | "green"
function getColor(type: "error" | "success"): string {
return type === "error" ? "red" : "green";
}
// Fix: pin the return type
function getColor(type: "error" | "success"): "red" | "green" {
return type === "error" ? "red" : "green";
}If the value comes from user input or a database, it is string at runtime regardless of the literal type. Use a type guard or Zod to validate it.
Real-world usage
- React / shadcn-ui:
variant: "primary" | "outline"in button props - tRPC:
type Method = "GET" | "POST"in route definitions - TanStack Query:
"pending" | "success" | "error"status types - Zod:
z.literal("published")for API schema validation
Follow-up questions
Q: What is the difference between type Foo = "a" and const foo = "a" as const?
A: type defines a type alias that exists only at the type level and disappears at compile time. as const creates a const variable whose inferred type is the literal "a". Both give the type "a", but as const also produces a runtime value.
Q: Can you use literal types with generics?
A: Yes. function identity<T extends "foo" | "bar">(x: T): T constrains the generic to a literal union. The caller gets back the exact literal, not a widened string.
Q: When should you prefer an enum over a literal union?
A: Literal unions compile away to nothing and are simpler to write. Enums generate a runtime object with reverse-mapping. For most cases in modern TypeScript, a literal union is enough.
Q: How do discriminated unions use literal types?
A: The literal field acts as a discriminant. TypeScript narrows the type automatically in each branch.
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; error: string };
// Inside a handler:
if (response.status === "success") {
console.log(response.data); // TypeScript knows data exists here
}Examples
Basic: direction handler
type Direction = "up" | "down" | "left" | "right";
function move(dir: Direction, distance: number) {
console.log(`Move ${distance}px ${dir}`);
}
move("up", 10); // OK - logs "Move 10px up"
move("left", 5); // OK
move("diagonal", 3); // Error: '"diagonal"' is not assignable to type 'Direction'TypeScript catches the invalid value before the code ships.
Intermediate: button component
type ButtonVariant = "primary" | "secondary" | "danger";
interface ButtonProps {
variant: ButtonVariant;
label: string;
}
function Button({ variant, label }: ButtonProps) {
return `<button class="btn btn-${variant}">${label}</button>`;
}
Button({ variant: "primary", label: "Save" }); // OK - renders btn-primary
Button({ variant: "warning", label: "Save" }); // Error: '"warning"' is not assignableThe component only accepts variants that have matching CSS classes. No runtime surprises.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.