HomeGuidesTypeScriptTypeScript Union & Intersection Types — Discriminated Unions Explained
🔷 TypeScript

TypeScript Union and Intersection Types

Union and intersection types model "or" and "and" relationships. Discriminated unions are the most powerful pattern.

Examifyr·2026·6 min read

Union types

A union type allows a value to be one of several types. Use | to separate them.

type StringOrNumber = string | number;

function display(value: StringOrNumber) {
    console.log(value);
}

// String literal unions (most common)
type Direction = "north" | "south" | "east" | "west";
type Status = "loading" | "success" | "error";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

function setStatus(status: Status) { ... }
setStatus("loading");   // OK
setStatus("pending");   // Error: not in union

Discriminated unions

A discriminated union uses a shared literal property to narrow between union members.

type LoadingState = {
    status: "loading";
};

type SuccessState = {
    status: "success";
    data: string[];
};

type ErrorState = {
    status: "error";
    error: string;
};

type State = LoadingState | SuccessState | ErrorState;

function render(state: State) {
    switch (state.status) {
        case "loading":
            return "Loading...";
        case "success":
            return state.data.join(", ");  // TypeScript knows data exists
        case "error":
            return `Error: ${state.error}`;  // TypeScript knows error exists
    }
}
Note: Discriminated unions are the recommended pattern for modelling state in TypeScript. The "discriminant" is the shared literal property (like status above).

Intersection types

An intersection type combines multiple types — the value must satisfy ALL of them.

type Named = { name: string };
type Aged  = { age: number };
type Employee = Named & Aged & { department: string };

const e: Employee = {
    name: "Alice",
    age: 30,
    department: "Engineering",
};

// Mixin pattern
type WithTimestamps<T> = T & {
    createdAt: Date;
    updatedAt: Date;
};

type UserWithTimestamps = WithTimestamps<User>;

Type narrowing

TypeScript narrows union types inside conditional blocks using type guards.

function processInput(value: string | number) {
    if (typeof value === "string") {
        // TypeScript knows value is string here
        return value.toUpperCase();
    } else {
        // TypeScript knows value is number here
        return value.toFixed(2);
    }
}

// instanceof narrowing
function handle(error: Error | string) {
    if (error instanceof Error) {
        console.log(error.message);  // Error properties available
    } else {
        console.log(error);
    }
}

// in operator narrowing
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function makeNoise(animal: Cat | Dog) {
    if ("meow" in animal) {
        animal.meow();
    } else {
        animal.bark();
    }
}

Exam tip

Discriminated unions are the most important TypeScript pattern for managing state. Always model async states (loading/success/error) as a discriminated union — it makes the impossible states unrepresentable.

🎯

Think you're ready? Prove it.

Take the free TypeScript readiness test. Get a score, topic breakdown, and your exact weak areas.

Take the free TypeScript test →

Free · No sign-up · Instant results

← Previous
TypeScript Type Inference Explained — When to Add Type Annotations
Next →
TypeScript Generics Explained — Generic Functions, Constraints & keyof
← All TypeScript guides