️ Type Guards

TypeScript Fundamentals — 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.