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
136: Equality Narrowing
You've likely encountered situations where a variable could be one of a few different types, and you just want TypeScript to get out of your way so you can call a method on that object. When I first started with TypeScript, my instinct was to just tell the compiler, "Trust me, I know what this is." That's a dangerous habit that leads to runtime crashes that the type system was specifically designed to prevent.
Fighting the compiler with type assertions
Let's look at a common scenario: handling a user's subscription status. Suppose you have a variable that is either a string representing the plan name or null if they aren't subscribed. The naive approach is to use a type assertion (the as keyword) to force the type when you think you've already handled the logic elsewhere.
function getPlanDetails(status: string | null) {
if (!status) {
return "No active plan";
}
// Naive approach: forcing the type
return (status as string).toUpperCase();
}
This works, but it's brittle. You're essentially bypassing the type checker. If someone changes the if (!status) check later to something less restrictive, or if the logic becomes more complex, that as string remains a blind spot. I've seen far too many production bugs where a developer "promised" the compiler a type was a string, but a null slipped through because the guard clause was slightly off. You're basically telling TypeScript to stop doing its job.
Letting the equality check do the heavy lifting
The better way is to use Equality Narrowing. TypeScript is smart enough to realize that if you've checked a variable against a specific value using === or !==, the type of that variable is narrowed within that block of code. You don't need to cast anything; the compiler just knows.
function getPlanDetails(status: string | null) {
if (status === null) {
return "No active plan";
}
// TypeScript now knows status MUST be a string here
return status.toUpperCase();
}
Notice how much cleaner that is. By checking for null explicitly, the rest of the function treats status as a string. This isn't just about saving a few keystrokes; it's about creating a "proven" path. If you try to change the logic in a way that makes status potentially null again, TypeScript will immediately flag the toUpperCase() call as an error. That's the safety net we actually want.
Narrowing with literal types
This gets even more powerful when you're dealing with union types of strings (literals). If you're checking for a specific value, TypeScript narrows the type to that exact string literal, which is incredibly useful for exhaustive checks.
type UserRole = "admin" | "editor" | "viewer";
function getPermissions(role: UserRole) {
if (role === "admin") {
// In this block, role is specifically the type "admin"
return ["read", "write", "delete"];
}
// Here, role is narrowed to "editor" | "viewer"
return ["read"];
}
I always prefer this approach because it makes the code self-documenting. Anyone reading the code can see exactly which conditions lead to which types. You aren't guessing based on a cast; you're following the logical flow of the data.
📋 Practical Task
Refactoring the API Response Guard
You have a function that processes an API response. The response can either be a User object or an ErrorResponse object. Currently, the code uses a type assertion to access the user's name, which is risky. Refactor the processResponse function to use equality narrowing instead of the as keyword to safely access the username.
type User = {
type: "success";
username: string;
};
type ErrorResponse = {
type: "error";
message: string;
};
type ApiResponse = User | ErrorResponse;
function processResponse(response: ApiResponse) {
if (response.type === "error") {
console.error("Error: " + response.message);
return;
}
// FIX THIS: Remove the type assertion and rely on narrowing
const user = response as User;
console.log("Welcome, " + user.username);
}There are no comments for now.