Beyond string and number: Unlocking Power with TypeScript Literal Types

TypeScript’s primitive types — string, number, boolean — are broad buckets. A literal type narrows that bucket to exactly one value, like "north" instead of any string, or 42 instead of any number. On their own, literals look trivial. But combined with union types, template literal types, and discriminated unions, they form the backbone of the precise, self-documenting APIs that make TypeScript genuinely safer than JavaScript. This guide explores every facet of literal types with practical, real-world examples.

What Is a Literal Type?

A literal type is a type whose only possible value is a specific literal:

// String literal type
type Direction = "north";          // only the string "north" is valid

// Number literal type
type HttpOk = 200;                 // only the number 200 is valid

// Boolean literal type
type AlwaysTrue = true;            // only true is valid

// These work exactly like you would expect:
let dir: Direction = "north";     // OK
let dir2: Direction = "south";    // ERROR: Type '"south"' is not assignable to type '"north"'

Literal Union Types — The Real Power

Literals become useful when combined with the union operator |. This creates a type that accepts a fixed set of values — essentially a type-safe enum alternative:

// String literal union
type Direction = "north" | "south" | "east" | "west";
type Status    = "pending" | "active" | "suspended" | "closed";
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

// Number literal union
type DiceRoll   = 1 | 2 | 3 | 4 | 5 | 6;
type HttpStatus = 200 | 201 | 400 | 401 | 403 | 404 | 500;

// Using them in functions
function move(direction: Direction, steps: number): void {
    console.log(`Moving ${steps} step(s) ${direction}`);
}

move("north", 3);    // OK
move("up",    1);    // ERROR: Argument of type '"up"' is not assignable to
                     //        parameter of type 'Direction'

// Autocomplete in IDEs shows all valid options
function setStatus(status: Status): void {
    console.log(`Status set to: ${status}`);
}
setStatus("active");     // OK
setStatus("deleted");    // ERROR

Literal Types vs enum

FeatureLiteral Union Typeenum
Syntax"a" | "b" | "c"enum X { A, B, C }
Compiled outputErased (zero runtime overhead)Compiled to a JS object
Iterable at runtimeNoYes (Object.values())
String values by defaultYesNumeric unless const enum or string enum
IDE autocompleteExcellentExcellent
Recommended?Yes (modern TS)Use only when runtime iteration needed

const Assertion — Widening Prevention

TypeScript widens let variables from literal to primitive type. Use as const to lock a value to its literal type:

// Without 'as const': TypeScript infers 'string', not '"north"'
let direction = "north";             // type: string

// With 'as const': TypeScript infers the literal '"north"'
const direction2 = "north" as const; // type: "north"

// 'as const' on objects -- every property becomes readonly and literal
const config = {
    host: "localhost",
    port: 3000,
    env:  "production"
} as const;
// config.host: "localhost" (not string)
// config.port: 3000        (not number)
// config.env:  "production" (not string)

// Very useful for deriving types from data
const ALLOWED_ROLES = ["admin", "editor", "viewer"] as const;
type Role = typeof ALLOWED_ROLES[number];  // "admin" | "editor" | "viewer"

Discriminated Unions

Literal types on a shared property create discriminated unions — the most powerful pattern in TypeScript for modelling state:

// Each shape has a literal 'kind' discriminant
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) {            // TypeScript narrows the type in each case
        case "circle":
            return Math.PI * shape.radius ** 2;    // shape is Circle here
        case "rectangle":
            return shape.width * shape.height;     // shape is Rectangle here
        case "triangle":
            return 0.5 * shape.base * shape.height;// shape is Triangle here
    }
    // TypeScript knows all cases are covered; no default needed
}

console.log(area({ kind: "circle",    radius: 5 }));           // 78.54
console.log(area({ kind: "rectangle", width: 4, height: 6 })); // 24
console.log(area({ kind: "triangle",  base: 3, height: 8 }));  // 12

Template Literal Types (TypeScript 4.1+)

TypeScript can compose string literal types using template literal syntax, enabling strongly typed string patterns:

type Color  = "red" | "blue" | "green";
type Size   = "small" | "medium" | "large";

// Cross-product: all combinations
type ColoredSize = `${Color}-${Size}`;
// "red-small" | "red-medium" | "red-large" |
// "blue-small" | ... | "green-large"

function applyStyle(style: ColoredSize): void {
    console.log(`Applying style: ${style}`);
}
applyStyle("blue-large");   // OK
applyStyle("pink-small");   // ERROR: not a valid ColoredSize

// Useful for typed event names
type Entity = "user" | "product" | "order";
type CrudEvent = `${Entity}:${ "created" | "updated" | "deleted" }`;
// "user:created" | "user:updated" | ... | "order:deleted"

function emit(event: CrudEvent): void { console.log(event); }
emit("user:created");   // OK
emit("user:archived");  // ERROR

Practical Example: API Response Handler

type ApiResponse<T> =
    | { status: "success"; data: T }
    | { status: "error";   message: string; code: 400 | 401 | 403 | 404 | 500 }
    | { status: "loading" };

function handle<T>(response: ApiResponse<T>): void {
    switch (response.status) {
        case "loading":
            console.log("Loading...");
            break;
        case "success":
            console.log("Data:", response.data);    // data is T here
            break;
        case "error":
            console.error(`Error ${response.code}: ${response.message}`);
            break;
    }
}

handle<string>({ status: "success", data: "Hello" });
handle<string>({ status: "error",   message: "Not found", code: 404 });

See Also

Conclusion

Literal types are one of TypeScript’s sharpest tools. On their own they enforce exact values; combined with unions they create type-safe finite sets of options without enum’s runtime overhead; with as const they prevent unwanted widening; with discriminated unions they enable exhaustive, type-narrowed switch statements; and with template literal types they compose string patterns at the type level. Reaching for literal types wherever you would otherwise use a plain string or number is one of the highest-leverage habits you can build as a TypeScript developer.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.