Literal Types
Most TypeScript types represent a set of values: string means any string, number means any number.
Literal types go one step further — they represent a single, exact value.
This makes them ideal for constraining variables to a small, known set of possibilities, like HTTP methods, button variants, or direction values.
String Literal Types
The most common form. A string literal type is just the quoted value used directly as a type.
// Only the exact string "left" is valid
type Direction = 'left';
const d: Direction = 'left'; // ✓
const e: Direction = 'right'; // Error: Type '"right"' is not assignable to type '"left"'
// More useful: a union of string literals
type Alignment = 'left' | 'center' | 'right';
function setAlignment(align: Alignment) {
console.log(`Alignment set to: ${align}`);
}
setAlignment('center'); // ✓
setAlignment('justify'); // Error: Argument of type '"justify"' is not assignableNumeric Literal Types
Numbers can also be literal types. They are useful for things like fixed status codes, dice faces, or binary flags.
type DiceFace = 1 | 2 | 3 | 4 | 5 | 6;
function roll(): DiceFace {
return (Math.floor(Math.random() * 6) + 1) as DiceFace;
}
type HttpOkStatus = 200 | 201 | 204;
type HttpErrorStatus = 400 | 401 | 403 | 404 | 500;
type HttpStatus = HttpOkStatus | HttpErrorStatus;
function handleResponse(status: HttpStatus) {
if (status === 200) {
console.log('OK');
} else if (status === 404) {
console.log('Not found');
}
}Boolean Literal Types
true and false are also valid literal types. On their own they are rarely useful, but they become
powerful in conditional types and discriminated unions.
type AlwaysTrue = true;
type AlwaysFalse = false;
// More practical: a tagged shape
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string };
function handleResponse<T>(res: ApiResponse<T>) {
if (res.success) {
// TypeScript knows res.data exists here
console.log(res.data);
} else {
// TypeScript knows res.error exists here
console.error(res.error);
}
}Template Literal Types
TypeScript 4.1 introduced template literal types — they work like JavaScript template strings but at the type level, letting you construct string literal types programmatically.
type Greeting = `Hello, ${string}`;
const g: Greeting = 'Hello, world'; // ✓
const h: Greeting = 'Hi, world'; // Error
// Combine literal unions to generate all combinations
type Axis = 'x' | 'y' | 'z';
type Event = `on${Capitalize<Axis>}`; // 'onX' | 'onY' | 'onZ'
type CSSProp = 'margin' | 'padding';
type Side = 'Top' | 'Right' | 'Bottom' | 'Left';
type CSSSpacingProp = `${CSSProp}${Side}`;
// => 'marginTop' | 'marginRight' | 'marginBottom' | 'marginLeft'
// | 'paddingTop' | 'paddingRight' | 'paddingBottom' | 'paddingLeft'Widening: const vs let
TypeScript infers different types depending on whether you use const or let. This is called
type widening — let variables can be reassigned, so TypeScript widens the type to the general form.
// const — TypeScript infers the literal type
const direction = 'left'; // type: "left"
const count = 42; // type: 42
const flag = true; // type: true
// let — TypeScript widens to the general type
let direction2 = 'left'; // type: string
let count2 = 42; // type: number
let flag2 = true; // type: boolean
// This matters when passing to a function expecting a literal type
type Alignment = 'left' | 'center' | 'right';
function align(a: Alignment) {}
const pos = 'left'; // type: "left" — assignable to Alignment ✓
let pos2 = 'left'; // type: string — NOT assignable to Alignment ✗
align(pos); // ✓
align(pos2); // Error: Argument of type 'string' is not assignable to type 'Alignment'let variable is widened to string, it no longer satisfies a union of string literals. Use as const or an explicit type annotation to fix this.// Fix 1: explicit annotation let pos3: Alignment = 'left'; // type: Alignment ✓ // Fix 2: const assertion let pos4 = 'left' as const; // type: "left" ✓ // Fix 3: use const const pos5 = 'left'; // type: "left" ✓
Discriminated Unions Using Literal Types
Literal types shine in discriminated unions — a pattern where each variant of a union carries a literal "tag" property that TypeScript uses to narrow the type inside conditionals.
type Circle = {
kind: 'circle';
radius: number;
};
type Rectangle = {
kind: 'rectangle';
width: number;
height: number;
};
type Triangle = {
kind: 'triangle';
base: number;
height: number;
};
type Shape = Circle | Rectangle | Triangle;
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// TypeScript knows shape is Circle here
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return 0.5 * shape.base * shape.height;
}
}kind discriminant is a string literal type. TypeScript uses it to narrowShape to exactly one variant inside each case branch — giving you full autocompletion and type safety with zero runtime overhead.Practical Example: Button Variants
Component libraries use literal types extensively for prop constraints. Here is a realistic example of a typed button component.
type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps {
variant?: ButtonVariant;
size?: ButtonSize;
disabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
function Button({ variant = 'primary', size = 'md', ...props }: ButtonProps) {
const classes = `btn btn--${variant} btn--${size}`;
return <button className={classes} {...props} />;
}
// Usage
<Button variant="danger" size="lg">Delete</Button> // ✓
<Button variant="huge">Submit</Button> // Error: "huge" not assignableHTTP Methods
Another real-world example: constraining HTTP methods so invalid strings are caught at compile time.
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
interface RequestConfig {
url: string;
method: HttpMethod;
body?: unknown;
}
function request(config: RequestConfig) {
return fetch(config.url, {
method: config.method,
body: config.body ? JSON.stringify(config.body) : undefined,
});
}
request({ url: '/api/users', method: 'GET' }); // ✓
request({ url: '/api/users', method: 'YEET' }); // Error
request({ url: '/api/users', method: 'POST', body: { name: 'Alice' } }); // ✓Using Literals as Function Parameter Constraints
You can pass a literal type as a generic constraint to ensure a function only accepts a known set of values while still preserving the exact literal for downstream use.
// Without literal constraint — loses specificity
function getField(key: string) { /* ... */ }
// With literal constraint — preserves the literal
function getField<K extends 'name' | 'email' | 'age'>(key: K) {
return key;
}
const k = getField('name'); // type: "name" (not just string)
// Using keyof for object fields
function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 30, email: 'alice@example.com' };
const name = pick(user, 'name'); // type: string
const age = pick(user, 'age'); // type: number
const bad = pick(user, 'phone'); // Error: "phone" doesn't existComparison Table
Literal Type | Example | Common Use Case |
|---|---|---|
String literal | 'left' | 'right' | Directions, variants, event names |
Numeric literal | 200 | 404 | 500 | HTTP status codes, dice faces |
Boolean literal | true | false | Discriminated unions, flags |
Template literal |
| Event names, CSS properties, routes |
Common Mistakes
Using
letwhen you need a literal type — TypeScript widens tostring/number. Useconstoras constinstead.Forgetting that template literal types are purely compile-time — they do not validate input at runtime.
Over-specifying with literals when a general type like
stringis genuinely what you want.Mixing numeric literals with enums unnecessarily — stick to one pattern per domain.
Quick Reference
Literal types represent a single exact value:
"left",42,trueCombine with
|to make a constrained set:"left" | "center" | "right"constinfers literal types;letwidens tostring/numberTemplate literal types construct string types at compile time
Discriminated unions use a literal tag property to enable type narrowing
Literal types have zero runtime cost — they exist only in the type system