Advanced Types
π Advanced Types
π 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
Check what type the value actually is, then use it accordingly.
π‘οΈ Type Guards
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
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
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'
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.
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.
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.
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.