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
140: User-Defined Type Guard Functions
I see this all the time when I'm reviewing PRs from developers moving to TypeScript: they write a helper function to check a type, it returns a boolean, and then they're baffled when TypeScript still screams at them for accessing a property that they know exists.
Thinking a boolean return is enough for narrowing
The misconception is that if a function returns true based on a type check, TypeScript will automatically "remember" that check in the calling code. It doesn't. Let's look at a real-world scenario: handling API responses that could either be a success or an error.
type SuccessResponse = { status: 'success'; data: string[] };
type ErrorResponse = { status: 'error'; message: string };
type ApiResponse = SuccessResponse | ErrorResponse;
function isSuccess(response: ApiResponse): boolean {
return response.status === 'success';
}
function handleResponse(response: ApiResponse) {
if (isSuccess(response)) {
// You'd think this works, right?
// WRONG. TypeScript still sees 'response' as ApiResponse.
console.log(response.data); // Error: Property 'data' does not exist on type 'ErrorResponse'.
}
}
Here's what's happening: isSuccess returns a boolean. To the TypeScript compiler, that function is a black box that returns true or false. It has no idea that the true value is logically tied to the SuccessResponse type. The "narrowing" happened inside the function, but it didn't leak back out to the caller.
Using type predicates to link logic to types
To fix this, we use a Type Predicate. Instead of returning boolean, we tell TypeScript exactly what the variable is if the function returns true using the parameterName is Type syntax.
type SuccessResponse = { status: 'success'; data: string[] };
type ErrorResponse = { status: 'error'; message: string };
type ApiResponse = SuccessResponse | ErrorResponse;
// Note the return type change here:
function isSuccess(response: ApiResponse): response is SuccessResponse {
return response.status === 'success';
}
function handleResponse(response: ApiResponse) {
if (isSuccess(response)) {
// Now this works perfectly.
// TypeScript knows that inside this block, response MUST be SuccessResponse.
console.log(response.data);
} else {
// And here, it knows it MUST be ErrorResponse.
console.log(response.message);
}
}
By changing the return type to response is SuccessResponse, you're creating a contract. You're telling the compiler: "If this function returns true, you can trust that the object I passed in is specifically a SuccessResponse."
I usually recommend these when your type-checking logic is complex or reused in multiple places. If you're just doing a quick check inside a single function, a simple if (response.status === 'success') is faster and achieves the same narrowing. But the moment you move that logic into its own function, the type predicate is non-negotiable.
📋 Practical Task
Implementing a User Permission Guard
You are building a dashboard that handles different types of User objects. Some are AdminUser (who have a permissions array) and some are GuestUser (who only have a guestId).
Currently, the code is failing because the developer used a standard boolean return for the type guard. Your task is to fix the isAdmin function so that the renderAdminPanel function can safely access the permissions property.
type AdminUser = {
role: 'admin';
permissions: string[];
name: string;
};
type GuestUser = {
role: 'guest';
guestId: string;
name: string;
};
type User = AdminUser | GuestUser;
// FIX THIS FUNCTION: Change the return type from 'boolean' to a type predicate
function isAdmin(user: User): boolean {
return user.role === 'admin';
}
function renderAdminPanel(user: User) {
if (isAdmin(user)) {
// This line is currently throwing a TS error
console.log(`Admin permissions: ${user.permissions.join(', ')}`);
} else {
console.log(`Guest ID: ${user.guestId}`);
}
}
// Test cases
renderAdminPanel({ role: 'admin', permissions: ['edit', 'delete'], name: 'Alice' });
renderAdminPanel({ role: 'guest', guestId: '12345', name: 'Bob' });
There are no comments for now.