Combine types in powerful ways with union and intersection types.

Union Types

A value can be one of several types. Use the | operator.

type StringOrNumber = string | number;

function processValue(value: StringOrNumber) {
  // value can be either string or number
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  } else {
    console.log(value.toFixed(2));
  }
}

Common Union Patterns

// Multiple primitives
type ID = string | number;

// With null/undefined
type MaybeString = string | null | undefined;

// With specific values
type Status = "pending" | "approved" | "rejected";

// Complex types
type Result = { success: true; data: any } | { success: false; error: string };

Discriminated Unions

Use a common property to distinguish between union members.

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function area(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;
  }
}

Intersection Types

A value must satisfy all types. Use the & operator.

interface Person {
  name: string;
  age: number;
}

interface Employee {
  employeeId: string;
  department: string;
}

type Staff = Person & Employee;

const staff: Staff = {
  name: "Alice",
  age: 30,
  employeeId: "E001",
  department: "Engineering"
  // Must have all properties from both interfaces
};

Merging Types

type A = { a: number };
type B = { b: string };
type C = A & B; // { a: number; b: string }

Intersection with Primitives

// Intersection of incompatible types results in 'never'
type Impossible = string & number; // never

Combining Union and Intersection

type A = { a: number } | { b: string };
type B = { c: boolean } & A;
// B is: ({ c: boolean; a: number }) | ({ c: boolean; b: string })

Type Narrowing

TypeScript narrows types based on control flow.

function process(value: string | number) {
  if (typeof value === "string") {
    // TypeScript knows value is string here
    console.log(value.length);
  } else {
    // TypeScript knows value is number here
    console.log(value.toFixed(2));
  }
}

Type Guards

interface Dog {
  type: "dog";
  bark(): void;
}

interface Cat {
  type: "cat";
  meow(): void;
}

type Pet = Dog | Cat;

function isDog(pet: Pet): pet is Dog {
  return pet.type === "dog";
}

function makeSound(pet: Pet) {
  if (isDog(pet)) {
    pet.bark(); // TypeScript knows it's a Dog
  } else {
    pet.meow(); // TypeScript knows it's a Cat
  }
}

Exercises

Exercise 1: Union Types

// Create a type 'ID' that can be string or number
// Create a function that accepts ID and returns a string representation

Exercise 2: Discriminated Union

// Create a discriminated union for API responses:
// - Success: { status: "success"; data: T }
// - Error: { status: "error"; message: string }
// Create a function that handles both cases

Exercise 3: Intersection Types

// Create two interfaces:
// - 'Readable' with read(): string
// - 'Writable' with write(data: string): void
// Create an intersection type 'ReadWrite' that has both

Exercise 4: Type Guards

// Create a type guard function 'isString' that:
// - Takes value: unknown
// - Returns value is string
// Use it to safely process a value

Exercise 5: Complex Union

// Create a union type for different event types:
// - ClickEvent: { type: "click"; x: number; y: number }
// - KeyEvent: { type: "key"; key: string }
// - MouseEvent: { type: "mouse"; button: number }
// Create a handler function for all event types

Best Practices

  1. Use discriminated unions - Makes type narrowing easier
  2. Prefer union over any - More type-safe
  3. Use type guards - Better than type assertions
  4. Name union types clearly - Makes code self-documenting
  5. Use intersection for composition - Combine multiple types

Common Mistakes

  1. Not narrowing union types
// ❌ Bad
function process(value: string | number) {
  value.toUpperCase(); // Error: Property doesn't exist on type 'number'
}

// ✅ Good
function process(value: string | number) {
  if (typeof value === "string") {
    value.toUpperCase();
  }
}
  1. Intersecting incompatible types
// ❌ Results in 'never'
type Bad = string & number;

// ✅ Use union instead
type Good = string | number;
  1. Forgetting discriminated property
// ❌ Hard to narrow
type Shape = { radius: number } | { width: number; height: number };

// ✅ Easy to narrow
type Shape = 
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number };

Next Steps

Congratulations! You've completed the Beginner level.
Proceed to Intermediate Level to learn about Generics, Utility Types, and more advanced concepts.