TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
27: Type Narrowing and Type Guards
You've probably run into this situation: you have a variable that could be one of a few different things—a union type—and you know exactly which one it is based on some logic in your code, but TypeScript is still screaming at you. It's frustrating because the logic is sound, but the compiler isn't seeing the connection.
interface User {
name: string;
email: string;
}
interface ApiError {
message: string;
code: number;
}
function handleResponse(response: User | ApiError) {
// ❌ TypeScript Error: Property 'email' does not exist on type 'User | ApiError'.
// Property 'email' does not exist on type 'ApiError'.
console.log(`Welcome back, ${response.email}!`);
}
The "Property does not exist" Wall
In the code above, I'm trying to treat the response as a User immediately. But TypeScript is playing it safe. Since response could be an ApiError, and errors don't have emails, TS blocks the build. It doesn't matter if you've checked the network tab and know the API returned a user; the compiler only cares about the type definition.
To fix this, we need to "narrow" the type. Narrowing is the process of moving from a less-specific type (the union) to a more-specific type (the individual member) using a type guard.
Narrowing with the 'in' operator
One of the quickest ways to fix this is using the in operator. This allows you to check if a specific property exists on an object at runtime, and TypeScript is smart enough to use that check to narrow the type for the rest of that block.
function handleResponse(response: User | ApiError) {
if ('email' in response) {
// Inside this block, TS knows 'response' must be a User
console.log(`Welcome back, ${response.email}!`);
} else {
// Here, TS knows it MUST be an ApiError
console.error(`Error ${response.code}: ${response.message}`);
}
}
This is clean, but it can get tedious if you're checking for the same type in ten different functions across your app. I hate repeating the same 'property' in object checks everywhere.
Creating Custom Type Guards
When your narrowing logic gets complex—or when you just want a reusable way to identify a type—you can write a User-Defined Type Guard. This is a function that returns a type predicate.
The magic happens in the return type: pet is Fish (or in our case, response is User). This tells TypeScript: "If this function returns true, you can officially treat this variable as this specific type."
function isUser(response: User | ApiError): response is User {
return (response as User).email !== undefined;
}
function handleResponse(response: User | ApiError) {
if (isUser(response)) {
// Now this is clean and reusable
console.log(`Welcome back, ${response.email}!`);
} else {
console.error(response.message);
}
}
I generally prefer this approach for any project that's larger than a few files. It moves the "how do I identify a User?" logic into one single place. If the User interface changes later, you only have to update the isUser function, not every if statement in your codebase.
📋 Practical Task
Exercise: Implementing a Notification Dispatcher
You are building a notification system that handles three different types of alerts: EmailNotification, SmsNotification, and PushNotification. Each has a unique property, but they all share a message property.
Your Goal: Create a custom type guard for each notification type and use them in a sendNotification function to log a specific delivery method for each.
interface EmailNotification {
message: string;
emailAddress: string;
}
interface SmsNotification {
message: string;
phoneNumber: string;
}
interface PushNotification {
message: string;
deviceId: string;
}
type Notification = EmailNotification | SmsNotification | PushNotification;
// 1. Implement these type guards
function isEmail(n: Notification): n is EmailNotification {
// Your code here
}
function isSms(n: Notification): n is SmsNotification {
// Your code here
}
function isPush(n: Notification): n is PushNotification {
// Your code here
}
function sendNotification(n: Notification) {
// 2. Use the guards to narrow the type and log the specific detail
// Example: "Sending Email to user@example.com: Hello!"
if (isEmail(n)) {
console.log(`Sending Email to ${n.emailAddress}: ${n.message}`);
}
// Implement the rest of the logic here...
}
// Test cases
sendNotification({ message: "Hello!", emailAddress: "test@test.com" });
sendNotification({ message: "Hi!", phoneNumber: "555-0123" });
sendNotification({ message: "Alert!", deviceId: "device_99" });There are no comments for now.