Skip to content
Union and Intersection Types
step 1/7

Reading — step 1 of 7

Learn

~3 min readType System Essentials

TypeScript's type system supports both union (|, OR) and intersection (&, AND) types. The TypeScript Handbook treats these as foundational — you'll use them constantly in real code.

Union types

A value that could be one of several types:

function format(input: string | number): string {
    if (typeof input === 'string') return input.toUpperCase();
    return input.toFixed(2);
}

format('hello');     // 'HELLO'
format(3.14159);     // '3.14'

The parameter accepts either type. Inside the function, you must narrow before using type-specific methods.

String literal unions — finite enums without enum:

type Direction = 'north' | 'south' | 'east' | 'west';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

function move(dir: Direction): void { /* ... */ }
move('north');       // OK
move('up');          // ERROR — not a Direction

This pattern is everywhere in TypeScript codebases. Constrains values at compile time without runtime overhead.

Intersection types

A value that satisfies multiple types simultaneously:

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

type Person = Named & Aged;

const p: Person = { name: 'Ada', age: 36 };    // must have BOTH

Useful for combining behaviors:

type Loggable = { log(msg: string): void };
type Serializable = { toJSON(): string };

function process(item: Loggable & Serializable) {
    item.log('processing');
    return item.toJSON();
}

The parameter must implement BOTH interfaces.

Discriminated unions — the killer pattern

Unions where each variant has a unique discriminator field:

type Shape =
    | { kind: 'circle'; radius: number }
    | { kind: 'square'; side: number }
    | { kind: 'triangle'; base: number; height: number };

function area(s: Shape): number {
    switch (s.kind) {
        case 'circle':   return Math.PI * s.radius ** 2;
        case 'square':   return s.side ** 2;
        case 'triangle': return 0.5 * s.base * s.height;
    }
}

TypeScript narrows s to the right variant inside each case based on kind. This is the type-safe alternative to inheritance hierarchies — model your domain as a closed set of cases.

Result types (Rust-influenced):

type Result<T> =
    | { ok: true; value: T }
    | { ok: false; error: string };

function parseAge(s: string): Result<number> {
    const n = parseInt(s);
    if (isNaN(n)) return { ok: false, error: 'not a number' };
    if (n < 0)    return { ok: false, error: 'negative' };
    return { ok: true, value: n };
}

const r = parseAge('42');
if (r.ok) {
    console.log(r.value);    // narrowed — value exists
} else {
    console.log(r.error);    // narrowed — error exists
}

No throw/catch noise; everything in the type system.

Common mistakes

  • Forgetting to narrow(input: string | number).toUpperCase() errors. Narrow with typeof first.
  • Using any to dodge unions — defeats the type system. Use proper unions and narrowing.
  • Treating intersection like inheritance — A & B is structural, not nominal. No method resolution order.
  • Empty unions or neverstring & number is never (no value can be both). Often a sign of a logic error.
  • Conflicting fields in intersections{ x: string } & { x: number } makes x have type never.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…