Skip to content
Union Types and Narrowing
step 1/5

Reading — step 1 of 5

Learn

~1 min readGenerics and Unions

Union types describe values that could be one of several:

let id: string | number;
id = 42;
id = "abc";

To USE a union, you must narrow — convince the compiler which case you're in:

function stringify(id: string | number): string {
    if (typeof id === "string") {
        return id.toUpperCase();    // narrowed to string
    }
    return id.toFixed(0);            // narrowed to number
}

Narrowing patterns:

  • typeof x === "string" — for primitives
  • x instanceof Class — for classes
  • "prop" in x — does the value have this property?
  • Array.isArray(x)
  • if (x !== null) — narrows away null/undefined
  • Discriminated unions — see below

Discriminated unions are the killer pattern. Each variant has a unique tag:

type Shape =
    | { kind: "circle"; radius: number }
    | { kind: "square"; side: number }
    | { kind: "rect"; width: number; height: number };

function area(s: Shape): number {
    switch (s.kind) {
        case "circle": return Math.PI * s.radius ** 2;     // s narrowed to circle
        case "square": return s.side ** 2;
        case "rect":   return s.width * s.height;
    }
}

The compiler checks exhaustiveness — add a new variant and all switch blocks light up.

Exhaustiveness pattern with never:

function area(s: Shape): number {
    switch (s.kind) {
        case "circle": ...;
        // omit a case to see the error
        default:
            const _exhaustive: never = s;   // compile error if Shape isn't fully covered
            return _exhaustive;
    }
}

Discussion

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

Sign in to post a comment or reply.

Loading…