TypeScriptLiteral Types

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.

TS
// 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 assignable
Note
A union of string literals is one of the most common patterns in TypeScript. It gives you the type-safety of an enum with the simplicity of plain strings.
Numeric Literal Types

Numbers can also be literal types. They are useful for things like fixed status codes, dice faces, or binary flags.

TS
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.

TS
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.

TS
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'
Tip
Template literal types are especially powerful when combined with mapped types to derive typed event names, CSS property names, or API route strings automatically.
Widening: const vs let

TypeScript infers different types depending on whether you use const or let. This is called type wideninglet variables can be reassigned, so TypeScript widens the type to the general form.

TS
// 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'
Warning
Widening is one of the most common sources of confusion for TypeScript beginners. When alet 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.

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

TS
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;
  }
}
Success
The 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.

TS
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 assignable
HTTP Methods

Another real-world example: constraining HTTP methods so invalid strings are caught at compile time.

TS
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.

TS
// 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 exist
Comparison 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

on${string}

Event names, CSS properties, routes

Common Mistakes
  1. Using let when you need a literal type — TypeScript widens to string / number. Use const or as const instead.

  2. Forgetting that template literal types are purely compile-time — they do not validate input at runtime.

  3. Over-specifying with literals when a general type like string is genuinely what you want.

  4. Mixing numeric literals with enums unnecessarily — stick to one pattern per domain.

Quick Reference
  • Literal types represent a single exact value: "left", 42, true

  • Combine with | to make a constrained set: "left" | "center" | "right"

  • const infers literal types; let widens to string / number

  • Template 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