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);
printID("user-123");
printID(true);
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) {
return x.toUpperCase();
}
✅ 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) {
if (typeof x === "string") {
return x.toUpperCase();
}
return x.toFixed(2);
}
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();
} else {
pet.bark();
}
}
Truthiness Guard
JavaScript truthiness checks narrow types:
function printLength(str: string | null) {
if (str) {
console.log(str.length);
}
}
🎯 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";
data: string;
};
type ErrorResponse = {
status: "error";
error: string;
};
type Response = SuccessResponse | ErrorResponse;
function handleResponse(response: Response) {
if (response.status === "success") {
console.log("Data:", response.data);
} else {
console.log("Error:", response.error);
}
}
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);
break;
case "error":
console.log("Error:", result.message);
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;
}
}
✅ 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:
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 {
}
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.