Skip to main content

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 any string
  • Main difference: string accepts "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

typescript
// 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"); // OK

The 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 need true only

as const: keeping literals from widening

typescript
// 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

typescript
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

typescript
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

typescript
// 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.

typescript
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

typescript
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

typescript
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 assignable

The component only accepts variants that have matching CSS classes. No runtime surprises.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?