Union & Intersection Types

Difficulty: Intermediate

Union types in TypeScript allow a value to be one of several types. You create a union using the pipe operator `|`. For example, `string | number` means the value can be either a string or a number. Unions are incredibly useful for modeling values that can legitimately take different forms, such as function parameters that accept multiple input types or API responses that might return different shapes.

When working with union types, TypeScript only allows you to access members that are common to all types in the union. To access type-specific members, you must narrow the type using type guards like `typeof`, `instanceof`, or custom checks. This narrowing is one of TypeScript's most powerful features - it gives you type safety while still allowing flexible inputs.

Intersection types are the complement of unions. Created with the `&` operator, an intersection type combines multiple types into one. A value of type `A & B` must satisfy both `A` and `B` simultaneously. This is commonly used to merge object types, creating a type that has all properties from each constituent type. Think of intersection as "AND" and union as "OR".

Intersections are particularly powerful for composing types from reusable pieces. Instead of defining one large interface, you can define small, focused interfaces and combine them with `&`. This approach aligns with the Interface Segregation Principle and makes your type definitions more modular and reusable across different parts of your application.

Code examples

Union Types with Narrowing

function formatValue(value: string | number | boolean): string {
  if (typeof value === "string") {
    return value.toUpperCase();
  } else if (typeof value === "number") {
    return value.toFixed(2);
  } else {
    return value ? "YES" : "NO";
  }
}

console.log(formatValue("hello"));
console.log(formatValue(3.14159));
console.log(formatValue(true));

The function accepts a union of three types. Inside each `typeof` branch, TypeScript narrows the type so you can safely call type-specific methods.

Intersection Types for Composing Objects

type HasName = { name: string };
type HasAge = { age: number };
type HasEmail = { email: string };

type Person = HasName & HasAge;
type ContactablePerson = Person & HasEmail;

function describe(person: ContactablePerson): string {
  return `${person.name}, age ${person.age}, email: ${person.email}`;
}

const user: ContactablePerson = {
  name: "Alice",
  age: 30,
  email: "alice@example.com"
};

console.log(describe(user));

Each small type defines one concern. Intersections combine them so ContactablePerson has all three properties. This modular approach keeps types reusable.

Union with Common Property

type Success = { status: "ok"; data: string };
type Failure = { status: "error"; message: string };
type Result = Success | Failure;

function handleResult(result: Result): string {
  if (result.status === "ok") {
    return `Data: ${result.data}`;
  } else {
    return `Error: ${result.message}`;
  }
}

console.log(handleResult({ status: "ok", data: "loaded" }));
console.log(handleResult({ status: "error", message: "not found" }));

The `status` property acts as a discriminant, allowing TypeScript to narrow the union based on its literal value. This is the foundation of discriminated unions.

Key points

Concepts covered

union types, intersection types, type narrowing, type composition