TypeScript

Literals in TypeScript: String, Number, Boolean, Template, and Compound Literals

Literals are one of the fundamental building blocks of TypeScript programming. They represent fixed values directly written into your code, forming the foundation upon which more complex type systems are built. Understanding literals deeply will significantly enhance your ability to write type-safe, expressive, and maintainable TypeScript code.

In this comprehensive guide, we’ll explore all aspects of TypeScript literals, from basic string, number, and boolean literals to advanced template literal types and compound literal patterns. Each section includes practical examples that demonstrate real-world applications.

What Are Literals?

A literal is a notation for representing a fixed value in source code. Unlike variables, which can change their values during program execution, literals are constant values that you write directly into your code. In TypeScript, literals serve a dual purpose: they are both runtime values and compile-time types.

String Literals

String literals represent textual data enclosed in quotes. TypeScript supports three ways to define string literals: single quotes, double quotes, and backticks (template literals).

Basic String Literals

// Single quotes
const greeting: string = 'Hello, World!';

// Double quotes
const farewell: string = "Goodbye, World!";

// Both are functionally identical
const message1 = 'TypeScript';
const message2 = "TypeScript";

String Literal Types

TypeScript allows you to use specific string values as types. This creates a type that only accepts that exact string value, making your code more precise and type-safe.

// String literal type
type Direction = 'north' | 'south' | 'east' | 'west';

function move(direction: Direction, distance: number): void {
    console.log(`Moving ${distance} meters to the ${direction}`);
}

move('north', 10); // ✓ Valid
move('northeast', 10); // ✗ Error: Argument of type 'northeast' is not assignable to parameter of type 'Direction'

Practical Example: API Response Status

type ApiStatus = 'idle' | 'loading' | 'success' | 'error';

interface ApiState<T> {
    status: ApiStatus;
    data: T | null;
    error: string | null;
}

class DataFetcher<T> {
    private state: ApiState<T> = {
        status: 'idle',
        data: null,
        error: null
    };

    async fetch(url: string): Promise<void> {
        this.state.status = 'loading';
        
        try {
            const response = await fetch(url);
            const data = await response.json();
            this.state = {
                status: 'success',
                data: data,
                error: null
            };
        } catch (error) {
            this.state = {
                status: 'error',
                data: null,
                error: error instanceof Error ? error.message : 'Unknown error'
            };
        }
    }

    getState(): ApiState<T> {
        return this.state;
    }
}

// Usage
const userFetcher = new DataFetcher<{ name: string; email: string }>();
userFetcher.fetch('https://api.example.com/users/1');

Escape Sequences in String Literals

String literals support escape sequences for special characters:

const multiline = 'First line\nSecond line\nThird line';
const tabSeparated = 'Name\tAge\tCity';
const quotedText = 'He said, "Hello!"';
const backslash = 'Path: C:\\Users\\Documents';
const unicode = 'Emoji: \u{1F600}'; // 😀

console.log(multiline);
// Output:
// First line
// Second line
// Third line

Number Literals

Number literals represent numeric values. TypeScript supports various numeric formats including decimal, hexadecimal, octal, and binary.

Basic Number Literals

// Decimal (base 10)
const decimal: number = 42;
const floatingPoint: number = 3.14159;
const scientific: number = 2.5e6; // 2,500,000

// Hexadecimal (base 16)
const hexadecimal: number = 0xFF; // 255

// Octal (base 8)
const octal: number = 0o77; // 63

// Binary (base 2)
const binary: number = 0b1010; // 10

Numeric Separators

TypeScript supports numeric separators (underscores) to improve readability of large numbers:

const largeNumber = 1_000_000_000; // One billion
const creditCardNumber = 1234_5678_9012_3456;
const bytes = 0b1111_0000_1010_0101;
const hexColor = 0xFF_00_FF; // Magenta

console.log(largeNumber); // 1000000000 (underscores are ignored at runtime)

Number Literal Types

Just like strings, you can use specific numeric values as types:

type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;

function rollDice(): DiceRoll {
    return (Math.floor(Math.random() * 6) + 1) as DiceRoll;
}

function evaluateRoll(roll: DiceRoll): string {
    switch (roll) {
        case 1:
            return 'Critical failure!';
        case 6:
            return 'Critical success!';
        default:
            return `You rolled a ${roll}`;
    }
}

const result = rollDice();
console.log(evaluateRoll(result));

Practical Example: HTTP Status Codes

type HttpSuccessCode = 200 | 201 | 204;
type HttpRedirectCode = 301 | 302 | 307 | 308;
type HttpClientErrorCode = 400 | 401 | 403 | 404 | 422;
type HttpServerErrorCode = 500 | 502 | 503;

type HttpStatusCode = 
    | HttpSuccessCode 
    | HttpRedirectCode 
    | HttpClientErrorCode 
    | HttpServerErrorCode;

interface HttpResponse<T> {
    statusCode: HttpStatusCode;
    body: T;
    headers: Record<string, string>;
}

class ApiResponse<T> {
    constructor(private response: HttpResponse<T>) {}

    isSuccess(): boolean {
        return this.response.statusCode >= 200 && this.response.statusCode < 300;
    }

    isRedirect(): boolean {
        return this.response.statusCode >= 300 && this.response.statusCode < 400;
    }

    isClientError(): boolean {
        return this.response.statusCode >= 400 && this.response.statusCode < 500;
    }

    isServerError(): boolean {
        return this.response.statusCode >= 500;
    }

    getData(): T | null {
        return this.isSuccess() ? this.response.body : null;
    }
}

// Usage
const response = new ApiResponse({
    statusCode: 200,
    body: { message: 'Success' },
    headers: { 'Content-Type': 'application/json' }
});

console.log(response.isSuccess()); // true
console.log(response.getData()); // { message: 'Success' }

Boolean Literals

Boolean literals represent logical values and have only two possible values: true and false.

Basic Boolean Literals

const isActive: boolean = true;
const isDeleted: boolean = false;

// Boolean expressions
const isAdult = age >= 18;
const hasPermission = isAdmin || isModerator;
const isValid = username.length > 3 && password.length >= 8;

Boolean Literal Types

While it might seem unusual at first, boolean literal types are incredibly useful for creating discriminated unions and precise type guards:

type Feature = {
    name: string;
    enabled: true; // Only accepts true
    config: Record<string, any>;
} | {
    name: string;
    enabled: false; // Only accepts false
    reason: string;
};

function processFeature(feature: Feature): void {
    if (feature.enabled) {
        // TypeScript knows feature has 'config' here
        console.log(`Feature ${feature.name} is enabled with config:`, feature.config);
    } else {
        // TypeScript knows feature has 'reason' here
        console.log(`Feature ${feature.name} is disabled: ${feature.reason}`);
    }
}

const myFeature: Feature = {
    name: 'DarkMode',
    enabled: true,
    config: { theme: 'dark', animations: true }
};

processFeature(myFeature);

Practical Example: Feature Flags System

interface EnabledFeatureFlag {
    name: string;
    enabled: true;
    rolloutPercentage: number;
    enabledFor: string[];
}

interface DisabledFeatureFlag {
    name: string;
    enabled: false;
    disabledReason: string;
    disabledAt: Date;
}

type FeatureFlag = EnabledFeatureFlag | DisabledFeatureFlag;

class FeatureFlagManager {
    private flags: Map<string, FeatureFlag> = new Map();

    addFlag(flag: FeatureFlag): void {
        this.flags.set(flag.name, flag);
    }

    isFeatureEnabled(featureName: string, userId: string): boolean {
        const flag = this.flags.get(featureName);
        
        if (!flag || !flag.enabled) {
            return false;
        }

        // TypeScript knows flag is EnabledFeatureFlag here
        if (flag.enabledFor.includes(userId)) {
            return true;
        }

        const randomValue = Math.random() * 100;
        return randomValue < flag.rolloutPercentage;
    }

    getDisabledReason(featureName: string): string | null {
        const flag = this.flags.get(featureName);
        
        if (!flag || flag.enabled) {
            return null;
        }

        // TypeScript knows flag is DisabledFeatureFlag here
        return flag.disabledReason;
    }
}

// Usage
const flagManager = new FeatureFlagManager();

flagManager.addFlag({
    name: 'newCheckout',
    enabled: true,
    rolloutPercentage: 50,
    enabledFor: ['admin-user-1', 'beta-tester-2']
});

flagManager.addFlag({
    name: 'experimentalFeature',
    enabled: false,
    disabledReason: 'Performance issues detected',
    disabledAt: new Date('2025-01-15')
});

console.log(flagManager.isFeatureEnabled('newCheckout', 'user-123')); // Maybe true/false based on rollout
console.log(flagManager.getDisabledReason('experimentalFeature')); // 'Performance issues detected'

Template String Literals

Template string literals (template literals) are string literals that allow embedded expressions and multi-line strings. They are enclosed in backticks (`) instead of quotes.

Basic Template Literals

const name = 'Alice';
const age = 30;

// String interpolation
const introduction = `My name is ${name} and I am ${age} years old.`;

// Expressions in template literals
const calculation = `The sum of 5 and 3 is ${5 + 3}`;

// Multi-line strings
const poem = `Roses are red,
Violets are blue,
TypeScript is awesome,
And so are you!`;

console.log(introduction); // My name is Alice and I am 30 years old.
console.log(calculation); // The sum of 5 and 3 is 8
console.log(poem);

Tagged Template Literals

Tagged template literals allow you to parse template literals with a function:

function highlight(strings: TemplateStringsArray, ...values: any[]): string {
    return strings.reduce((result, str, i) => {
        const value = values[i] !== undefined ? `<mark>${values[i]}</mark>` : '';
        return result + str + value;
    }, '');
}

const searchTerm = 'TypeScript';
const sentence = highlight`I love ${searchTerm} programming!`;
console.log(sentence); // I love <mark>TypeScript</mark> programming!

Template Literal Types

One of TypeScript’s most powerful features is template literal types, which allow you to create new string literal types by combining existing ones:

type Environment = 'dev' | 'staging' | 'prod';
type Region = 'us' | 'eu' | 'asia';

// Create compound string literal type
type ServerUrl = `https://${Environment}-${Region}.example.com`;

// This type expands to:
// "https://dev-us.example.com" | "https://dev-eu.example.com" | "https://dev-asia.example.com" |
// "https://staging-us.example.com" | "https://staging-eu.example.com" | ... and so on

function connectToServer(url: ServerUrl): void {
    console.log(`Connecting to ${url}`);
}

connectToServer('https://dev-us.example.com'); // ✓ Valid
connectToServer('https://prod-eu.example.com'); // ✓ Valid
connectToServer('https://test-us.example.com'); // ✗ Error

Practical Example: Type-Safe Event System

type EventType = 'user' | 'product' | 'order';
type EventAction = 'created' | 'updated' | 'deleted';

// Generate all possible event names
type EventName = `${EventType}:${EventAction}`;

interface EventPayload {
    timestamp: Date;
    userId: string;
}

interface UserEventPayload extends EventPayload {
    userName: string;
    email: string;
}

interface ProductEventPayload extends EventPayload {
    productId: string;
    productName: string;
}

interface OrderEventPayload extends EventPayload {
    orderId: string;
    amount: number;
}

type EventMap = {
    'user:created': UserEventPayload;
    'user:updated': UserEventPayload;
    'user:deleted': UserEventPayload;
    'product:created': ProductEventPayload;
    'product:updated': ProductEventPayload;
    'product:deleted': ProductEventPayload;
    'order:created': OrderEventPayload;
    'order:updated': OrderEventPayload;
    'order:deleted': OrderEventPayload;
};

class TypeSafeEventEmitter {
    private listeners: {
        [K in EventName]?: Array<(payload: EventMap[K]) => void>;
    } = {};

    on<E extends EventName>(
        eventName: E,
        callback: (payload: EventMap[E]) => void
    ): void {
        if (!this.listeners[eventName]) {
            this.listeners[eventName] = [];
        }
        this.listeners[eventName]!.push(callback);
    }

    emit<E extends EventName>(eventName: E, payload: EventMap[E]): void {
        const callbacks = this.listeners[eventName];
        if (callbacks) {
            callbacks.forEach(callback => callback(payload));
        }
    }
}

// Usage
const eventEmitter = new TypeSafeEventEmitter();

// Type-safe listener
eventEmitter.on('user:created', (payload) => {
    // TypeScript knows payload is UserEventPayload
    console.log(`User created: ${payload.userName} (${payload.email})`);
});

eventEmitter.on('order:created', (payload) => {
    // TypeScript knows payload is OrderEventPayload
    console.log(`Order created: ${payload.orderId} for $${payload.amount}`);
});

// Type-safe emission
eventEmitter.emit('user:created', {
    timestamp: new Date(),
    userId: 'user-123',
    userName: 'John Doe',
    email: 'john@example.com'
});

// This would cause a compile error because the payload doesn't match
// eventEmitter.emit('user:created', { orderId: '123' }); // ✗ Error

Advanced Template Literal Types: String Manipulation

TypeScript provides intrinsic string manipulation types that work with template literal types:

// Uppercase
type UppercaseGreeting = Uppercase<'hello'>; // "HELLO"

// Lowercase
type LowercaseGreeting = Lowercase<'HELLO'>; // "hello"

// Capitalize
type CapitalizedName = Capitalize<'john'>; // "John"

// Uncapitalize
type UncapitalizedName = Uncapitalize<'John'>; // "john"

// Practical example: Generate getter/setter names
type PropertyName = 'firstName' | 'lastName' | 'email';

type Getter<T extends string> = `get${Capitalize<T>}`;
type Setter<T extends string> = `set${Capitalize<T>}`;

type GetterNames = Getter<PropertyName>; // "getFirstName" | "getLastName" | "getEmail"
type SetterNames = Setter<PropertyName>; // "setFirstName" | "setLastName" | "setEmail"

class User {
    private firstName: string = '';
    private lastName: string = '';
    private email: string = '';

    getFirstName(): string {
        return this.firstName;
    }

    setFirstName(value: string): void {
        this.firstName = value;
    }

    getLastName(): string {
        return this.lastName;
    }

    setLastName(value: string): void {
        this.lastName = value;
    }

    getEmail(): string {
        return this.email;
    }

    setEmail(value: string): void {
        this.email = value;
    }
}

Compound Literals

Compound literals combine multiple literal types to create more complex type structures. They leverage union types, intersection types, and object literal types to build sophisticated type systems.

Union Types with Literals

Union types allow a value to be one of several literal types:

type Status = 'pending' | 'approved' | 'rejected';
type Priority = 'low' | 'medium' | 'high' | 'critical';
type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl';

interface Task {
    id: number;
    title: string;
    status: Status;
    priority: Priority;
}

function updateTaskStatus(task: Task, newStatus: Status): Task {
    return { ...task, status: newStatus };
}

const myTask: Task = {
    id: 1,
    title: 'Review pull request',
    status: 'pending',
    priority: 'high'
};

const updatedTask = updateTaskStatus(myTask, 'approved');

Object Literal Types

Object literal types define the exact shape of an object:

type Point2D = {
    x: number;
    y: number;
};

type Point3D = {
    x: number;
    y: number;
    z: number;
};

type Color = {
    r: number;
    g: number;
    b: number;
    a?: number; // Optional alpha channel
};

type ColoredPoint = Point3D & Color; // Intersection type

const coloredPoint: ColoredPoint = {
    x: 10,
    y: 20,
    z: 5,
    r: 255,
    g: 128,
    b: 0,
    a: 0.8
};

Discriminated Unions

Discriminated unions (tagged unions) combine literal types with object types to create powerful type-safe patterns:

interface Circle {
    kind: 'circle';
    radius: number;
}

interface Rectangle {
    kind: 'rectangle';
    width: number;
    height: number;
}

interface Triangle {
    kind: 'triangle';
    base: number;
    height: number;
}

type Shape = Circle | Rectangle | Triangle;

function calculateArea(shape: Shape): number {
    switch (shape.kind) {
        case 'circle':
            // TypeScript knows shape is Circle here
            return Math.PI * shape.radius ** 2;
        case 'rectangle':
            // TypeScript knows shape is Rectangle here
            return shape.width * shape.height;
        case 'triangle':
            // TypeScript knows shape is Triangle here
            return (shape.base * shape.height) / 2;
    }
}

const myCircle: Circle = { kind: 'circle', radius: 5 };
const myRectangle: Rectangle = { kind: 'rectangle', width: 10, height: 20 };

console.log(calculateArea(myCircle)); // 78.54
console.log(calculateArea(myRectangle)); // 200

Practical Example: State Machine

type LoadingState = {
    status: 'loading';
    progress: number;
};

type SuccessState<T> = {
    status: 'success';
    data: T;
    timestamp: Date;
};

type ErrorState = {
    status: 'error';
    error: Error;
    retryCount: number;
};

type IdleState = {
    status: 'idle';
};

type AsyncState<T> = IdleState | LoadingState | SuccessState<T> | ErrorState;

class StateMachine<T> {
    private state: AsyncState<T> = { status: 'idle' };
    private listeners: Array<(state: AsyncState<T>) => void> = [];

    getState(): AsyncState<T> {
        return this.state;
    }

    subscribe(listener: (state: AsyncState<T>) => void): () => void {
        this.listeners.push(listener);
        return () => {
            this.listeners = this.listeners.filter(l => l !== listener);
        };
    }

    private setState(newState: AsyncState<T>): void {
        this.state = newState;
        this.listeners.forEach(listener => listener(this.state));
    }

    async execute(asyncFn: () => Promise<T>): Promise<void> {
        this.setState({ status: 'loading', progress: 0 });

        try {
            // Simulate progress updates
            const progressInterval = setInterval(() => {
                const currentState = this.state;
                if (currentState.status === 'loading' && currentState.progress < 90) {
                    this.setState({ 
                        status: 'loading', 
                        progress: currentState.progress + 10 
                    });
                }
            }, 100);

            const data = await asyncFn();
            clearInterval(progressInterval);

            this.setState({
                status: 'success',
                data,
                timestamp: new Date()
            });
        } catch (error) {
            const currentState = this.state;
            const retryCount = currentState.status === 'error' 
                ? currentState.retryCount + 1 
                : 0;

            this.setState({
                status: 'error',
                error: error instanceof Error ? error : new Error('Unknown error'),
                retryCount
            });
        }
    }

    reset(): void {
        this.setState({ status: 'idle' });
    }
}

// Usage
const machine = new StateMachine<{ users: string[] }>();

machine.subscribe((state) => {
    switch (state.status) {
        case 'idle':
            console.log('Machine is idle');
            break;
        case 'loading':
            console.log(`Loading: ${state.progress}%`);
            break;
        case 'success':
            console.log('Success!', state.data);
            console.log('Loaded at:', state.timestamp);
            break;
        case 'error':
            console.error('Error occurred:', state.error.message);
            console.log('Retry count:', state.retryCount);
            break;
    }
});

// Execute async operation
machine.execute(async () => {
    await new Promise(resolve => setTimeout(resolve, 1000));
    return { users: ['Alice', 'Bob', 'Charlie'] };
});

Practical Example: Configuration System

type Environment = 'development' | 'staging' | 'production';
type LogLevel = 'debug' | 'info' | 'warn' | 'error';

interface DatabaseConfig {
    host: string;
    port: number;
    database: string;
    ssl: boolean;
}

interface CacheConfig {
    enabled: boolean;
    ttl: number;
    maxSize: number;
}

interface AppConfig {
    environment: Environment;
    logLevel: LogLevel;
    database: DatabaseConfig;
    cache: CacheConfig;
    features: {
        enableBetaFeatures: boolean;
        maintenanceMode: boolean;
    };
}

const developmentConfig: AppConfig = {
    environment: 'development',
    logLevel: 'debug',
    database: {
        host: 'localhost',
        port: 5432,
        database: 'myapp_dev',
        ssl: false
    },
    cache: {
        enabled: true,
        ttl: 300,
        maxSize: 100
    },
    features: {
        enableBetaFeatures: true,
        maintenanceMode: false
    }
};

const productionConfig: AppConfig = {
    environment: 'production',
    logLevel: 'error',
    database: {
        host: 'db.example.com',
        port: 5432,
        database: 'myapp_prod',
        ssl: true
    },
    cache: {
        enabled: true,
        ttl: 3600,
        maxSize: 1000
    },
    features: {
        enableBetaFeatures: false,
        maintenanceMode: false
    }
};

class ConfigManager {
    private config: AppConfig;

    constructor(environment: Environment) {
        this.config = this.loadConfig(environment);
    }

    private loadConfig(environment: Environment): AppConfig {
        switch (environment) {
            case 'development':
                return developmentConfig;
            case 'staging':
                return { ...productionConfig, logLevel: 'info', environment: 'staging' };
            case 'production':
                return productionConfig;
        }
    }

    get<K extends keyof AppConfig>(key: K): AppConfig[K] {
        return this.config[key];
    }

    getDatabaseUrl(): string {
        const db = this.config.database;
        const protocol = db.ssl ? 'postgresql+ssl' : 'postgresql';
        return `${protocol}://${db.host}:${db.port}/${db.database}`;
    }

    shouldLog(level: LogLevel): boolean {
        const levels: LogLevel[] = ['debug', 'info', 'warn', 'error'];
        const currentLevelIndex = levels.indexOf(this.config.logLevel);
        const requestedLevelIndex = levels.indexOf(level);
        return requestedLevelIndex >= currentLevelIndex;
    }
}

// Usage
const config = new ConfigManager('production');
console.log(config.get('environment')); // 'production'
console.log(config.getDatabaseUrl()); // 'postgresql+ssl://db.example.com:5432/myapp_prod'
console.log(config.shouldLog('debug')); // false
console.log(config.shouldLog('error')); // true

Advanced Patterns: Mapped Types with Literals

Mapped types allow you to transform literal types programmatically:

type Permission = 'read' | 'write' | 'delete' | 'admin';

// Create an object type with all permissions as boolean properties
type PermissionMap = {
    [K in Permission]: boolean;
};

// This expands to:
// {
//     read: boolean;
//     write: boolean;
//     delete: boolean;
//     admin: boolean;
// }

interface User {
    id: string;
    username: string;
    permissions: PermissionMap;
}

class PermissionManager {
    private user: User;

    constructor(user: User) {
        this.user = user;
    }

    hasPermission(permission: Permission): boolean {
        return this.user.permissions[permission];
    }

    grantPermission(permission: Permission): void {
        this.user.permissions[permission] = true;
    }

    revokePermission(permission: Permission): void {
        this.user.permissions[permission] = false;
    }

    getAllPermissions(): Permission[] {
        return (Object.keys(this.user.permissions) as Permission[])
            .filter(perm => this.user.permissions[perm]);
    }
}

// Usage
const user: User = {
    id: 'user-001',
    username: 'johndoe',
    permissions: {
        read: true,
        write: true,
        delete: false,
        admin: false
    }
};

const manager = new PermissionManager(user);
console.log(manager.hasPermission('read')); // true
console.log(manager.hasPermission('delete')); // false
console.log(manager.getAllPermissions()); // ['read', 'write']

Best Practices

1. Prefer Literal Types Over Enums for Simple Cases

// Good: Clear and concise
type Status = 'active' | 'inactive' | 'suspended';

// Less ideal for simple cases: More verbose
enum StatusEnum {
    Active = 'active',
    Inactive = 'inactive',
    Suspended = 'suspended'
}

2. Use Discriminated Unions for Complex State

// Good: Type-safe with discriminated union
type Result<T, E = Error> = 
    | { success: true; value: T }
    | { success: false; error: E };

function processResult<T>(result: Result<T>): void {
    if (result.success) {
        console.log(result.value); // TypeScript knows value exists
    } else {
        console.error(result.error); // TypeScript knows error exists
    }
}

3. Leverage Template Literal Types for API Routes

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiVersion = 'v1' | 'v2';
type Resource = 'users' | 'products' | 'orders';

type ApiRoute = `/api/${ApiVersion}/${Resource}`;

function makeRequest(method: HttpMethod, route: ApiRoute): void {
    console.log(`${method} ${route}`);
}

makeRequest('GET', '/api/v1/users'); // ✓ Valid
makeRequest('POST', '/api/v2/products'); // ✓ Valid
// makeRequest('GET', '/api/v3/users'); // ✗ Error

4. Combine Literals with Generics for Flexibility

type Action<T extends string, P = void> = P extends void
    ? { type: T }
    : { type: T; payload: P };

type UserActions =
    | Action<'USER_LOGIN', { username: string; password: string }>
    | Action<'USER_LOGOUT'>
    | Action<'USER_UPDATE_PROFILE', { name: string; email: string }>;

function handleAction(action: UserActions): void {
    switch (action.type) {
        case 'USER_LOGIN':
            console.log(`Logging in user: ${action.payload.username}`);
            break;
        case 'USER_LOGOUT':
            console.log('Logging out user');
            break;
        case 'USER_UPDATE_PROFILE':
            console.log(`Updating profile: ${action.payload.name}`);
            break;
    }
}

Conclusion

Literals in TypeScript are far more than simple constant values. They form the foundation of TypeScript’s powerful type system, enabling you to create precise, self-documenting, and type-safe code. From basic string and number literals to advanced template literal types and discriminated unions, understanding these concepts deeply will significantly improve your TypeScript development experience.

Key takeaways:

  1. String literals provide exact string values as types, perfect for states, modes, and configurations
  2. Number literals enable precise numeric constraints, ideal for status codes, versions, and discrete values
  3. Boolean literals create type-safe discriminated unions and feature flag systems
  4. Template string literals allow dynamic string type generation and string manipulation at the type level
  5. Compound literals combine multiple literal types to build sophisticated, type-safe architectures

By mastering these literal types and patterns, you’ll write code that catches errors at compile-time rather than runtime, provides better IDE autocomplete support, and serves as living documentation for your codebase.

Additional Advanced Patterns

Recursive Template Literal Types

Template literal types can be used recursively to create sophisticated string patterns:

type PathSegment = string;
type DeepPath<T, K extends keyof T = keyof T> = K extends string
    ? T[K] extends object
        ? `${K}` | `${K}.${DeepPath<T[K]>}`
        : `${K}`
    : never;

interface Config {
    database: {
        host: string;
        credentials: {
            username: string;
            password: string;
        };
    };
    server: {
        port: number;
        ssl: boolean;
    };
}

// This generates all possible deep paths:
// "database" | "database.host" | "database.credentials" | 
// "database.credentials.username" | "database.credentials.password" |
// "server" | "server.port" | "server.ssl"
type ConfigPath = DeepPath<Config>;

function getConfigValue(path: ConfigPath): any {
    // Implementation would traverse the config object
    console.log(`Getting value at path: ${path}`);
}

getConfigValue('database.credentials.username'); // ✓ Valid
getConfigValue('server.port'); // ✓ Valid
// getConfigValue('database.invalid.path'); // ✗ Error

Const Assertions

The as const assertion creates readonly literal types from values:

// Without const assertion
const colors1 = ['red', 'green', 'blue'];
// Type: string[]

// With const assertion
const colors2 = ['red', 'green', 'blue'] as const;
// Type: readonly ["red", "green", "blue"]

// Object with const assertion
const config = {
    apiUrl: 'https://api.example.com',
    timeout: 5000,
    retries: 3
} as const;
// All properties become readonly literal types

type ApiUrl = typeof config.apiUrl; // "https://api.example.com"
type Timeout = typeof config.timeout; // 5000

// Practical example: Route definitions
const ROUTES = {
    home: '/',
    about: '/about',
    contact: '/contact',
    user: {
        profile: '/user/profile',
        settings: '/user/settings',
        posts: '/user/posts'
    }
} as const;

type RouteValue = typeof ROUTES[keyof typeof ROUTES];
// "/" | "/about" | "/contact" | { profile: "/user/profile", ... }

function navigateTo(route: string): void {
    console.log(`Navigating to: ${route}`);
}

navigateTo(ROUTES.home); // ✓ Valid
navigateTo(ROUTES.user.profile); // ✓ Valid

Practical Example: Type-Safe Query Builder

type Operator = '=' | '!=' | '>' | '<' | '>=' | '<=' | 'LIKE' | 'IN';
type LogicalOperator = 'AND' | 'OR';

interface WhereClause<T> {
    field: keyof T;
    operator: Operator;
    value: any;
}

interface Query<T> {
    table: string;
    select?: (keyof T)[];
    where?: WhereClause<T>[];
    logicalOperator?: LogicalOperator;
    orderBy?: {
        field: keyof T;
        direction: 'ASC' | 'DESC';
    };
    limit?: number;
}

class QueryBuilder<T> {
    private query: Query<T>;

    constructor(table: string) {
        this.query = { table };
    }

    select(...fields: (keyof T)[]): this {
        this.query.select = fields;
        return this;
    }

    where(field: keyof T, operator: Operator, value: any): this {
        if (!this.query.where) {
            this.query.where = [];
        }
        this.query.where.push({ field, operator, value });
        return this;
    }

    and(field: keyof T, operator: Operator, value: any): this {
        this.query.logicalOperator = 'AND';
        return this.where(field, operator, value);
    }

    or(field: keyof T, operator: Operator, value: any): this {
        this.query.logicalOperator = 'OR';
        return this.where(field, operator, value);
    }

    orderBy(field: keyof T, direction: 'ASC' | 'DESC' = 'ASC'): this {
        this.query.orderBy = { field, direction };
        return this;
    }

    limit(count: number): this {
        this.query.limit = count;
        return this;
    }

    build(): string {
        const { table, select, where, logicalOperator, orderBy, limit } = this.query;
        
        let sql = 'SELECT ';
        sql += select ? select.join(', ') : '*';
        sql += ` FROM ${table}`;

        if (where && where.length > 0) {
            const operator = logicalOperator || 'AND';
            const conditions = where.map(w => 
                `${String(w.field)} ${w.operator} ${this.formatValue(w.value)}`
            ).join(` ${operator} `);
            sql += ` WHERE ${conditions}`;
        }

        if (orderBy) {
            sql += ` ORDER BY ${String(orderBy.field)} ${orderBy.direction}`;
        }

        if (limit) {
            sql += ` LIMIT ${limit}`;
        }

        return sql;
    }

    private formatValue(value: any): string {
        if (typeof value === 'string') {
            return `'${value}'`;
        }
        if (Array.isArray(value)) {
            return `(${value.map(v => this.formatValue(v)).join(', ')})`;
        }
        return String(value);
    }
}

// Usage
interface User {
    id: number;
    username: string;
    email: string;
    age: number;
    isActive: boolean;
}

const query = new QueryBuilder<User>('users')
    .select('id', 'username', 'email')
    .where('isActive', '=', true)
    .and('age', '>=', 18)
    .orderBy('username', 'ASC')
    .limit(10)
    .build();

console.log(query);
// SELECT id, username, email FROM users WHERE isActive = true AND age >= 18 ORDER BY username ASC LIMIT 10

// This would cause a TypeScript error (invalid field name):
// new QueryBuilder<User>('users').where('invalidField', '=', true);

Practical Example: Type-Safe Form Validation

type ValidationRule = 
    | { type: 'required'; message: string }
    | { type: 'minLength'; value: number; message: string }
    | { type: 'maxLength'; value: number; message: string }
    | { type: 'pattern'; value: RegExp; message: string }
    | { type: 'custom'; validate: (value: any) => boolean; message: string };

type ValidationResult = 
    | { valid: true }
    | { valid: false; errors: string[] };

interface FieldValidation<T> {
    field: keyof T;
    rules: ValidationRule[];
}

class FormValidator<T extends Record<string, any>> {
    private validations: FieldValidation<T>[] = [];

    addValidation(field: keyof T, rules: ValidationRule[]): this {
        this.validations.push({ field, rules });
        return this;
    }

    validate(data: T): Record<keyof T, ValidationResult> {
        const results = {} as Record<keyof T, ValidationResult>;

        for (const validation of this.validations) {
            const { field, rules } = validation;
            const value = data[field];
            const errors: string[] = [];

            for (const rule of rules) {
                const error = this.validateRule(value, rule);
                if (error) {
                    errors.push(error);
                }
            }

            results[field] = errors.length > 0 
                ? { valid: false, errors }
                : { valid: true };
        }

        return results;
    }

    private validateRule(value: any, rule: ValidationRule): string | null {
        switch (rule.type) {
            case 'required':
                return !value || (typeof value === 'string' && value.trim() === '')
                    ? rule.message
                    : null;
            
            case 'minLength':
                return typeof value === 'string' && value.length < rule.value
                    ? rule.message
                    : null;
            
            case 'maxLength':
                return typeof value === 'string' && value.length > rule.value
                    ? rule.message
                    : null;
            
            case 'pattern':
                return typeof value === 'string' && !rule.value.test(value)
                    ? rule.message
                    : null;
            
            case 'custom':
                return !rule.validate(value)
                    ? rule.message
                    : null;
        }
    }

    isValid(data: T): boolean {
        const results = this.validate(data);
        return Object.values(results).every(result => result.valid);
    }
}

// Usage
interface RegistrationForm {
    username: string;
    email: string;
    password: string;
    confirmPassword: string;
    age: number;
}

const validator = new FormValidator<RegistrationForm>()
    .addValidation('username', [
        { type: 'required', message: 'Username is required' },
        { type: 'minLength', value: 3, message: 'Username must be at least 3 characters' },
        { type: 'maxLength', value: 20, message: 'Username must be at most 20 characters' }
    ])
    .addValidation('email', [
        { type: 'required', message: 'Email is required' },
        { 
            type: 'pattern', 
            value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, 
            message: 'Invalid email format' 
        }
    ])
    .addValidation('password', [
        { type: 'required', message: 'Password is required' },
        { type: 'minLength', value: 8, message: 'Password must be at least 8 characters' },
        {
            type: 'custom',
            validate: (value: string) => /[A-Z]/.test(value) && /[0-9]/.test(value),
            message: 'Password must contain at least one uppercase letter and one number'
        }
    ])
    .addValidation('age', [
        { type: 'required', message: 'Age is required' },
        {
            type: 'custom',
            validate: (value: number) => value >= 18,
            message: 'You must be at least 18 years old'
        }
    ]);

const formData: RegistrationForm = {
    username: 'jo',
    email: 'invalid-email',
    password: 'weak',
    confirmPassword: 'weak',
    age: 16
};

const validationResults = validator.validate(formData);

console.log('Username validation:', validationResults.username);
// { valid: false, errors: ['Username must be at least 3 characters'] }

console.log('Email validation:', validationResults.email);
// { valid: false, errors: ['Invalid email format'] }

console.log('Password validation:', validationResults.password);
// { valid: false, errors: ['Password must be at least 8 characters', 'Password must contain...'] }

console.log('Is form valid?', validator.isValid(formData));
// false

Practical Example: Type-Safe Router

type RouteParams<Path extends string> = 
    Path extends `${infer Start}:${infer Param}/${infer Rest}`
        ? { [K in Param | keyof RouteParams<Rest>]: string }
        : Path extends `${infer Start}:${infer Param}`
            ? { [K in Param]: string }
            : {};

type RouteDefinition<Path extends string> = {
    path: Path;
    handler: (params: RouteParams<Path>) => void;
};

class TypeSafeRouter {
    private routes: Map<string, (params: any) => void> = new Map();

    register<Path extends string>(
        route: RouteDefinition<Path>
    ): void {
        this.routes.set(route.path, route.handler);
    }

    navigate<Path extends string>(
        path: Path,
        params: RouteParams<Path>
    ): void {
        // Find matching route
        for (const [routePath, handler] of this.routes) {
            const match = this.matchRoute(routePath, path, params);
            if (match) {
                handler(match);
                return;
            }
        }
        console.error(`No route found for: ${path}`);
    }

    private matchRoute(
        routePath: string,
        actualPath: string,
        params: any
    ): Record<string, string> | null {
        const routeParts = routePath.split('/');
        const actualParts = actualPath.split('/');

        if (routeParts.length !== actualParts.length) {
            return null;
        }

        const extractedParams: Record<string, string> = {};

        for (let i = 0; i < routeParts.length; i++) {
            if (routeParts[i].startsWith(':')) {
                const paramName = routeParts[i].slice(1);
                extractedParams[paramName] = params[paramName];
            } else if (routeParts[i] !== actualParts[i]) {
                return null;
            }
        }

        return extractedParams;
    }
}

// Usage
const router = new TypeSafeRouter();

router.register({
    path: '/users/:userId',
    handler: (params) => {
        // TypeScript knows params has userId
        console.log(`User profile for ID: ${params.userId}`);
    }
});

router.register({
    path: '/posts/:postId/comments/:commentId',
    handler: (params) => {
        // TypeScript knows params has postId and commentId
        console.log(`Post ${params.postId}, Comment ${params.commentId}`);
    }
});

// Type-safe navigation
router.navigate('/users/:userId', { userId: '123' }); // ✓ Valid
router.navigate('/posts/:postId/comments/:commentId', { 
    postId: '456', 
    commentId: '789' 
}); // ✓ Valid

// This would cause a TypeScript error (missing required params):
// router.navigate('/posts/:postId/comments/:commentId', { postId: '456' });

Conditional Literal Types

You can create conditional types based on literal values:

type IsString<T> = T extends string ? true : false;
type IsNumber<T> = T extends number ? true : false;

type CheckType = IsString<'hello'>; // true
type CheckType2 = IsString<42>; // false

// Practical example: Extract specific types from union
type ExtractStrings<T> = T extends string ? T : never;

type Mixed = 'apple' | 42 | 'banana' | true | 'cherry';
type OnlyStrings = ExtractStrings<Mixed>; // 'apple' | 'banana' | 'cherry'

// Filter object properties by value type
type FilterByValueType<T, ValueType> = {
    [K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};

interface Product {
    id: number;
    name: string;
    description: string;
    price: number;
    inStock: boolean;
}

type StringFields = FilterByValueType<Product, string>;
// { name: string; description: string; }

type NumberFields = FilterByValueType<Product, number>;
// { id: number; price: number; }

Practical Example: Type-Safe Environment Variables

type EnvVarName = 
    | 'DATABASE_URL'
    | 'API_KEY'
    | 'NODE_ENV'
    | 'PORT'
    | 'LOG_LEVEL';

type NodeEnv = 'development' | 'staging' | 'production' | 'test';
type LogLevel = 'debug' | 'info' | 'warn' | 'error';

interface EnvVarTypes {
    'DATABASE_URL': string;
    'API_KEY': string;
    'NODE_ENV': NodeEnv;
    'PORT': number;
    'LOG_LEVEL': LogLevel;
}

class EnvironmentConfig {
    private env: Partial<EnvVarTypes> = {};

    constructor() {
        this.loadEnvironmentVariables();
    }

    private loadEnvironmentVariables(): void {
        // In a real application, this would read from process.env
        this.env = {
            'DATABASE_URL': 'postgresql://localhost:5432/mydb',
            'API_KEY': 'secret-key-123',
            'NODE_ENV': 'development',
            'PORT': 3000,
            'LOG_LEVEL': 'debug'
        };
    }

    get<K extends EnvVarName>(key: K): EnvVarTypes[K] {
        const value = this.env[key];
        
        if (value === undefined) {
            throw new Error(`Environment variable ${key} is not defined`);
        }

        return value as EnvVarTypes[K];
    }

    getOrDefault<K extends EnvVarName>(
        key: K,
        defaultValue: EnvVarTypes[K]
    ): EnvVarTypes[K] {
        const value = this.env[key];
        return value !== undefined ? (value as EnvVarTypes[K]) : defaultValue;
    }

    isProduction(): boolean {
        return this.get('NODE_ENV') === 'production';
    }

    isDevelopment(): boolean {
        return this.get('NODE_ENV') === 'development';
    }

    shouldLogLevel(level: LogLevel): boolean {
        const levels: LogLevel[] = ['debug', 'info', 'warn', 'error'];
        const currentLevel = this.get('LOG_LEVEL');
        return levels.indexOf(level) >= levels.indexOf(currentLevel);
    }
}

// Usage
const env = new EnvironmentConfig();

// Type-safe access
const dbUrl: string = env.get('DATABASE_URL');
const port: number = env.get('PORT');
const nodeEnv: NodeEnv = env.get('NODE_ENV');

console.log(`Server running on port ${port} in ${nodeEnv} mode`);
console.log(`Should log debug? ${env.shouldLogLevel('debug')}`);

// With default values
const timeout = env.getOrDefault('PORT', 8080);

Performance Considerations

When working with literal types, especially complex template literal types and mapped types, be aware of potential performance implications:

// This can be slow to compile with many combinations
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type Version = 'v1' | 'v2' | 'v3' | 'v4';
type Resource = 'users' | 'products' | 'orders' | 'payments' | 'reviews';

// This creates 5 * 4 * 5 = 100 literal types
type AllRoutes = `/${Version}/${Resource}`;

// Consider using string for very large combinations
type RoutePattern = `/${string}/${string}`;

// Or break into smaller, more manageable pieces
type UserRoutes = `/v1/${'users' | 'profiles'}`;
type ProductRoutes = `/v1/${'products' | 'categories'}`;
type Routes = UserRoutes | ProductRoutes;

Testing with Literal Types

Literal types make testing more reliable by catching type errors at compile time:

interface TestCase<Input, Expected> {
    name: string;
    input: Input;
    expected: Expected;
}

type MathOperation = 'add' | 'subtract' | 'multiply' | 'divide';

function calculate(a: number, b: number, operation: MathOperation): number {
    switch (operation) {
        case 'add':
            return a + b;
        case 'subtract':
            return a - b;
        case 'multiply':
            return a * b;
        case 'divide':
            if (b === 0) throw new Error('Division by zero');
            return a / b;
    }
}

const testCases: TestCase<{ a: number; b: number; op: MathOperation }, number>[] = [
    { name: 'Addition', input: { a: 5, b: 3, op: 'add' }, expected: 8 },
    { name: 'Subtraction', input: { a: 10, b: 4, op: 'subtract' }, expected: 6 },
    { name: 'Multiplication', input: { a: 6, b: 7, op: 'multiply' }, expected: 42 },
    { name: 'Division', input: { a: 15, b: 3, op: 'divide' }, expected: 5 },
    // This would cause a TypeScript error:
    // { name: 'Invalid', input: { a: 1, b: 2, op: 'power' }, expected: 1 },
];

function runTests(): void {
    testCases.forEach(test => {
        const result = calculate(test.input.a, test.input.b, test.input.op);
        const passed = result === test.expected;
        console.log(`${test.name}: ${passed ? '✓ PASS' : '✗ FAIL'} (expected ${test.expected}, got ${result})`);
    });
}

runTests();

Summary

Literals in TypeScript provide a powerful mechanism for creating precise, type-safe code. By leveraging string, number, boolean, template, and compound literals, you can:

  • Eliminate entire classes of runtime errors by catching mistakes at compile time
  • Improve code documentation through self-describing types
  • Enable better IDE support with accurate autocomplete and inline documentation
  • Create sophisticated type systems that model complex business logic
  • Build type-safe APIs that prevent misuse and guide correct usage

Whether you’re building a simple utility function or a complex application architecture, understanding and effectively using TypeScript literals will make your code more maintainable, reliable, and enjoyable to work with. The examples in this article demonstrate real-world patterns that you can adapt and apply to your own projects, from state machines and routers to validation systems and configuration management.

Related posts
TypeScript

Utility Types in TypeScript: Pick, Omit, Partial, Required, Readonly, Record...

✅ — Utility Types are one of the most powerful features of TypeScript, but also one of the most…
Read more
TypeScript

Complete TypeScript Tutorial Online: Master TypeScript in 2025

TypeScript has revolutionized modern web development by bringing static typing to JavaScript, making…
Read more
TypeScript

Unions vs Intersections in TypeScript – Complete Guide with Real Examples

This is one of the most important comparative topics in TypeScript: Unions vs Intersections.Let’s…
Read more