TechLead
Lesson 5 of 8
5 min read
TypeScript

Union & Intersection Types

Combine types with unions and intersections, and use type guards for narrowing

Union Types

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

// Basic union type
let id: string | number;
id = "abc123";  // OK
id = 123;       // OK
// id = true;   // Error!

// Union in function parameters
function printId(id: string | number): void {
  console.log("ID:", id);
}

// Union with arrays
let mixedArray: (string | number)[] = [1, "two", 3, "four"];

// Union with literal types
type Direction = "north" | "south" | "east" | "west";
let heading: Direction = "north";

// Union with null/undefined
type MaybeString = string | null | undefined;

function processName(name: string | null): string {
  if (name === null) {
    return "Anonymous";
  }
  return name.toUpperCase();
}

Type Narrowing

Narrow union types to specific types using type guards:

// typeof narrowing
function padLeft(value: string, padding: string | number): string {
  if (typeof padding === "number") {
    // padding is number here
    return " ".repeat(padding) + value;
  }
  // padding is string here
  return padding + value;
}

// truthiness narrowing
function processValue(value: string | null | undefined): string {
  if (value) {
    // value is string here (truthy)
    return value.toUpperCase();
  }
  return "default";
}

// equality narrowing
function compare(a: string | number, b: string | boolean): void {
  if (a === b) {
    // a and b are both string here
    console.log(a.toUpperCase());
  }
}

// in operator narrowing
type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird): void {
  if ("swim" in animal) {
    animal.swim();  // animal is Fish
  } else {
    animal.fly();   // animal is Bird
  }
}

// instanceof narrowing
function logDate(date: Date | string): void {
  if (date instanceof Date) {
    console.log(date.toISOString());  // date is Date
  } else {
    console.log(date);  // date is string
  }
}

Discriminated Unions

Use a common property to discriminate between union members:

// Discriminated union with 'kind' property
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 getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
  }
}

// Exhaustiveness checking
function assertNever(x: never): never {
  throw new Error("Unexpected value: " + x);
}

function getAreaExhaustive(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default:
      return assertNever(shape);  // Error if case missing
  }
}

Intersection Types

Intersection types combine multiple types into one:

// Basic intersection
type Person = {
  name: string;
  age: number;
};

type Employee = {
  employeeId: number;
  department: string;
};

type Staff = Person & Employee;

const staff: Staff = {
  name: "Alice",
  age: 30,
  employeeId: 12345,
  department: "Engineering"
};

// Intersection with interfaces
interface Timestamps {
  createdAt: Date;
  updatedAt: Date;
}

interface Identifiable {
  id: string;
}

type Entity<T> = T & Timestamps & Identifiable;

type UserEntity = Entity<{ name: string; email: string }>;

const user: UserEntity = {
  id: "user-1",
  name: "Bob",
  email: "bob@example.com",
  createdAt: new Date(),
  updatedAt: new Date()
};

User-Defined Type Guards

// Type predicate function
interface Cat {
  meow(): void;
  purr(): void;
}

interface Dog {
  bark(): void;
  wagTail(): void;
}

// Type guard with 'is' keyword
function isCat(animal: Cat | Dog): animal is Cat {
  return "meow" in animal;
}

function interact(animal: Cat | Dog): void {
  if (isCat(animal)) {
    animal.meow();   // animal is Cat
    animal.purr();
  } else {
    animal.bark();   // animal is Dog
    animal.wagTail();
  }
}

// Type guard for API response
interface SuccessResponse {
  success: true;
  data: unknown;
}

interface ErrorResponse {
  success: false;
  error: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function isSuccess(response: ApiResponse): response is SuccessResponse {
  return response.success === true;
}

function handleResponse(response: ApiResponse): void {
  if (isSuccess(response)) {
    console.log("Data:", response.data);
  } else {
    console.error("Error:", response.error);
  }
}

Practical Examples

// Redux action types
type LoadUsersAction = {
  type: "LOAD_USERS";
};

type AddUserAction = {
  type: "ADD_USER";
  payload: { name: string; email: string };
};

type RemoveUserAction = {
  type: "REMOVE_USER";
  payload: { id: number };
};

type UserAction = LoadUsersAction | AddUserAction | RemoveUserAction;

function userReducer(state: User[], action: UserAction): User[] {
  switch (action.type) {
    case "LOAD_USERS":
      return state;
    case "ADD_USER":
      return [...state, { id: Date.now(), ...action.payload }];
    case "REMOVE_USER":
      return state.filter(u => u.id !== action.payload.id);
  }
}

// React component props with variants
type ButtonProps = {
  label: string;
  onClick: () => void;
} & (
  | { variant: "primary"; icon?: never }
  | { variant: "icon"; icon: string }
);

// Usage:
// 

Key Takeaways

  • • Union types (|) allow one of several types
  • • Intersection types (&) combine types
  • • Use type guards to narrow union types
  • • Discriminated unions use a common property
  • • Custom type guards use is keyword
  • • Exhaustiveness checking catches missing cases

Continue Learning