TypeScript Union Types

A union type allows a variable to hold a value of more than one type. Instead of restricting a variable to a single type, a union says "this value can be type A or type B." The pipe symbol | separates each type in the union. Union types handle real-world data that naturally comes in multiple forms — an ID that can be a number or a string, for example.

What Is a Union?

  Single type:           Union type:
  +----------+           +----------+
  | number   |           | number   |
  |          |     OR    |    OR    |
  +----------+           | string   |
                         +----------+

  let age: number         let id: number | string
  age = 25;  ✓           id = 1001;      ✓
  age = "25"; ✗          id = "EMP-101"; ✓
                          id = true;     ✗

Declaring a Union Type

// Variable that accepts both number and string
let employeeId: number | string;

employeeId = 1001;         // Valid — number
employeeId = "EMP-1001";   // Valid — string
employeeId = true;         // Error: boolean not in union

// Union with three types
let config: string | number | boolean;
config = "dark";   // Valid
config = 42;       // Valid
config = false;    // Valid

Union in Function Parameters

function formatId(id: number | string): string {
    return "ID: " + id.toString();
}

console.log(formatId(1001));       // ID: 1001
console.log(formatId("EMP-1001")); // ID: EMP-1001

Narrowing — Working Safely with Unions

When a variable has a union type, TypeScript does not know which specific type it holds at any moment. Before using type-specific operations, the code must check the actual type. This process is called type narrowing.

  Union: number | string
         |
    +-----------+
    |           |
  number      string
         |
  Before narrowing — only methods common to both types available
  After narrowing  — all methods of the narrowed type available
function processInput(input: number | string): string {
    if (typeof input === "string") {
        // Narrowed to string — string methods available
        return input.toUpperCase();
    }
    // Narrowed to number — number methods available
    return input.toFixed(2);
}

console.log(processInput("hello")); // HELLO
console.log(processInput(3.14159)); // 3.14

Narrowing with instanceof

class Dog {
    bark(): void { console.log("Woof!"); }
}

class Cat {
    meow(): void { console.log("Meow!"); }
}

function makeSound(animal: Dog | Cat): void {
    if (animal instanceof Dog) {
        animal.bark(); // Narrowed to Dog
    } else {
        animal.meow(); // Narrowed to Cat
    }
}

makeSound(new Dog()); // Woof!
makeSound(new Cat()); // Meow!

Union Types with Arrays

// Array of mixed types
let mixedList: (number | string)[] = [1, "two", 3, "four"];

// Array that accepts either all numbers or all strings
let data: number[] | string[] = [1, 2, 3];
data = ["a", "b", "c"]; // Valid reassignment

Literal Union Types

A union of specific literal values restricts a variable to only those exact values. This works like a custom enum using string or number literals.

type Direction = "north" | "south" | "east" | "west";
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
type Size = "small" | "medium" | "large" | "xl";

let heading: Direction = "north";
heading = "east";    // Valid
heading = "up";      // Error: '"up"' not assignable to type 'Direction'

let roll: DiceRoll = 6;
roll = 7; // Error: 7 not in literal union

let tshirtSize: Size = "medium";

Union in Object Properties

type Notification = {
    id: number;
    message: string;
    type: "info" | "warning" | "error" | "success";
    timestamp: Date | string;
};

let alert: Notification = {
    id: 1,
    message: "Your order has been placed.",
    type: "success",
    timestamp: new Date()
};

let legacyAlert: Notification = {
    id: 2,
    message: "Server maintenance at midnight.",
    type: "warning",
    timestamp: "2025-06-10T00:00:00"  // string format also accepted
};

Discriminated Unions

A discriminated union uses a common property (the discriminant) to tell types apart. TypeScript uses the discriminant value to narrow to the exact type in each branch.

  Shape diagram:
  +------------+      +------------+      +------------+
  | Circle     |      | Rectangle  |      | Triangle   |
  | kind:"circ"|      | kind:"rect"|      | kind:"tri" |
  | radius     |      | width      |      | base       |
  +------------+      | height     |      | height     |
                      +------------+      +------------+
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 getArea(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius * shape.radius;
        case "rectangle":
            return shape.width * shape.height;
        case "triangle":
            return 0.5 * shape.base * shape.height;
    }
}

console.log(getArea({ kind: "circle", radius: 5 }));
// Output: 78.53...

console.log(getArea({ kind: "rectangle", width: 4, height: 6 }));
// Output: 24

console.log(getArea({ kind: "triangle", base: 8, height: 5 }));
// Output: 20

Practical Example

// API response handler
type ApiSuccess = {
    status: "success";
    data: string[];
    count: number;
};

type ApiError = {
    status: "error";
    errorCode: number;
    message: string;
};

type ApiResponse = ApiSuccess | ApiError;

function handleResponse(response: ApiResponse): void {
    if (response.status === "success") {
        console.log("Data received: " + response.count + " items");
        response.data.forEach(item => console.log(" - " + item));
    } else {
        console.log("Error " + response.errorCode + ": " + response.message);
    }
}

handleResponse({ status: "success", data: ["TypeScript", "React"], count: 2 });
// Data received: 2 items
//  - TypeScript
//  - React

handleResponse({ status: "error", errorCode: 404, message: "Not Found" });
// Error 404: Not Found

Leave a Comment