Advanced Types

TypeScript Fundamentals
Course 1 Β· Chapter 7 Β· Advanced Types

πŸ”€ Advanced Types

You've seen union types in passing. Now we'll master them. Union types are where TypeScript shines β€” they let you express that a value can be one of several types, and TypeScript will help you handle each case correctly. This chapter teaches type guards, discriminated unions, and exhaustiveness checking.

πŸ”€ Union Types Deep Dive

Basic Unions

A union type says "this can be one of these types":

type ID = string | number;

function printID(id: ID) {
  console.log(id);
}

printID(42);  // βœ… Works: number is OK
printID("user-123");  // βœ… Works: string is OK
printID(true);  // ❌ Error! boolean is not in the union

The Problem: Accessing Properties

With unions, TypeScript only knows about properties that exist on ALL union members:

type Padded = string | number;

function stringify(x: Padded) {
  // ❌ Error! Property 'toUpperCase' does not exist on type 'number'
  return x.toUpperCase();
}

// Problem: toUpperCase exists on strings but NOT on numbers
// So TypeScript forbids it to prevent runtime errors
βœ… Solution: Type Guards

Check what type the value actually is, then use it accordingly.

πŸ›‘οΈ Type Guards

A type guard is code that checks the type of a variable and narrows it to a more specific type. TypeScript then knows you've verified the type and allows you to use type-specific properties.

typeof Guard

type Padded = string | number;

function stringify(x: Padded) {
  // typeof guard: check if it's a string
  if (typeof x === "string") {
    return x.toUpperCase();  // βœ… Now TypeScript knows it's a string
  }
  // Here, x must be number
  return x.toFixed(2);  // βœ… Now TypeScript knows it's a number
}

instanceof Guard

For classes, use instanceof:

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

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

function petSound(pet: Cat | Dog) {
  if (pet instanceof Cat) {
    pet.meow();  // βœ… TypeScript knows it's a Cat
  } else {
    pet.bark();  // βœ… TypeScript knows it's a Dog
  }
}

Truthiness Guard

JavaScript truthiness checks narrow types:

function printLength(str: string | null) {
  if (str) {
    // Inside this block, str cannot be null
    console.log(str.length);  // βœ… Safe
  }
}

🎯 Discriminated Unions

A discriminated union (also called tagged unions) is a union of types that share a common propertyβ€”the "discriminator"β€”which you can use to determine the exact type. This pattern is incredibly powerful and type-safe.

The Pattern

type SuccessResponse = {
  status: "success";  // Discriminator: always "success"
  data: string;
};

type ErrorResponse = {
  status: "error";  // Discriminator: always "error"
  error: string;
};

type Response = SuccessResponse | ErrorResponse;

function handleResponse(response: Response) {
  // Check the discriminator (status property)
  if (response.status === "success") {
    // TypeScript narrows to SuccessResponse
    console.log("Data:", response.data);  // βœ… 'data' only exists here
  } else {
    // TypeScript narrows to ErrorResponse
    console.log("Error:", response.error);  // βœ… 'error' only exists here
  }
}

With switch Statement

Discriminated unions shine with switch statements:

type Result =
  | { status: "pending" }
  | { status: "success"; value: number }
  | { status: "error"; message: string };

function handle(result: Result) {
  switch (result.status) {
    case "pending":
      console.log("Waiting...");
      break;
    case "success":
      console.log("Result:", result.value);  // βœ… 'value' available
      break;
    case "error":
      console.log("Error:", result.message);  // βœ… 'message' available
      break;
  }
}

βœ… Exhaustiveness Checking

TypeScript can verify you've handled all possible cases in a union. This catches bugs when you add a new type to the union but forget to handle it somewhere.
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number }
  | { kind: "triangle"; side: number };

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.side ** 2;
    // ❌ Error! Not all cases covered (missing 'triangle')
  }
}

// TypeScript error: Function lacks ending return statement and
// return type does not include 'undefined'
βœ… Fix: Handle all cases
case "triangle":
  return (shape.side ** 2) * Math.sqrt(3) / 4;

🚫 The never Type

The never type represents impossible values. Use it for exhaustiveness checking:

function assertNever(x: never): never {
  throw new Error("Should not reach here");
}

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.side ** 2;
    case "triangle":
      return (shape.side ** 2) * Math.sqrt(3) / 4;
    default:
      // If you add a new Shape type and forget to handle it here,
      // TypeScript will complain that the new type doesn't match 'never'
      return assertNever(shape);
  }
}

πŸ’» Coding Challenges

Challenge 1: Type Guard Function

Create a type guard function that checks if a value is a string. Use it in a function that only proceeds if the guard passes.

function isString(value: unknown): boolean {
  // Your implementation
}

Goal: Demonstrate typeof guard pattern.

β†’ Solution

Challenge 2: Discriminated Union

Create a discriminated union for API responses with three states: loading, success (with data), and error (with message). Write a function to handle each.

  • Define the union type
  • Write a handler function using switch statement
  • Test all three cases

Goal: Demonstrate discriminated unions and type narrowing.

β†’ Solution

Challenge 3: Exhaustiveness with never

Create a union of three animal types with a makeSound() function. Use the never type to ensure all cases are handled.

  • Define three animal types (Cat, Dog, Bird)
  • Each has different sound methods
  • Use assertNever in the default case

Goal: Demonstrate exhaustiveness checking with never type.

β†’ Solution

πŸ’‘ Real-World Tip: Discriminated Unions > Many Conditions

Instead of checking multiple boolean flags or scattered properties, use discriminated unions to represent distinct states explicitly. This catches bugs at compile-time and makes code self-documenting. The pattern is used extensively in React (status states), error handling (Result types), and state machines.

🎯 What's Next

We've mastered unions and type narrowing. Chapter 8 covers Functions & Overloads β€” how to type functions that behave differently based on their arguments, and when to use function overloads.