How Type Predicates Clean Up Your TypeScript
Type predicates are one of TypeScript’s most underused features. They let you define custom type guards that narrow types in your control flow—so TypeScript understands exactly what shape your data has at each point in your code. The result? Cleaner logic, no type assertions, and fewer runtime surprises.
The Problem: Repetitive Type Checking
Your dashboard fetches data from an API. Sometimes it returns a success object with user info, other times an error with a message. Both are valid responses, but you need different handling for each.
Here’s what the types might look like:
type SuccessResponse = {
status: 'success';
data: {
name: string;
email: string;
id: number;
};
};
type ErrorResponse = {
status: 'error';
message: string;
code: number;
};
type ApiResponse = SuccessResponse | ErrorResponse;
Without type guards, you check the same properties everywhere, and TypeScript doesn’t always follow along:
// This gets repetitive fast
if (response.status === 'success') {
// TypeScript might still be confused here without proper narrowing
console.log(response.data.name); // Potential type error
}
The Solution: Type Predicates
Instead of repeated inline checks, write a function that tells TypeScript how to identify each type:
function isSuccessResponse(res: ApiResponse): res is SuccessResponse {
return res.status === 'success';
}
The res is SuccessResponse part is the type predicate. It tells TypeScript: “If this function returns true, treat the parameter as a SuccessResponse in the calling scope.”
Now your code becomes straightforward:
if (isSuccessResponse(response)) {
// TypeScript knows this is SuccessResponse
console.log(`User: ${response.data.name}`);
console.log(`Email: ${response.data.email}`);
} else {
// TypeScript knows this is ErrorResponse
console.error(`Error ${response.code}: ${response.message}`);
}
Complex Type Discrimination
Type predicates shine when dealing with multiple entity types. Consider a system that returns either users or teams:
type User = {
type: 'user';
username: string;
age: number;
lastLogin: Date;
};
type Team = {
type: 'team';
teamName: string;
members: string[];
created: Date;
};
type Entity = User | Team;
Create a guard for each type:
function isUser(entity: Entity): entity is User {
return entity.type === 'user';
}
function isTeam(entity: Entity): entity is Team {
return entity.type === 'team';
}
Your business logic stays focused:
function processEntity(entity: Entity) {
if (isUser(entity)) {
console.log(`User ${entity.username} last logged in ${entity.lastLogin}`);
// Update user analytics
} else if (isTeam(entity)) {
console.log(`Team ${entity.teamName} has ${entity.members.length} members`);
// Send team notifications
}
}
Watch Out For These Gotchas
Type predicates are powerful but require care:
- Your predicate must be correct. TypeScript trusts your implementation. If your predicate returns true for the wrong type, you’ll get runtime errors:
// WRONG: This will cause runtime errors
function isBadUser(entity: Entity): entity is User {
return true; // Always returns true!
}
- Keep predicates simple. Complex validation logic belongs elsewhere. Type predicates should check type shape, not business rules:
// Good: Checks structure
function isUser(entity: Entity): entity is User {
return entity.type === 'user' && 'username' in entity;
}
// Bad: Mixing concerns
function isValidUser(entity: Entity): entity is User {
return (
entity.type === 'user' &&
entity.age >= 18 && // Business logic doesn't belong here
entity.username.length > 3
);
}
- Arrays need special handling. When filtering arrays, TypeScript needs help understanding the result:
const entities: Entity[] = [
/* ... */
];
// Without type predicate, users is still Entity[]
const users = entities.filter((e) => e.type === 'user');
// With type predicate, users is User[]
const users = entities.filter(isUser);
When Not to Use Type Predicates
Skip type predicates when:
- You’re only checking once (inline checks work fine)
- The type difference is trivial (a single optional property)
- You’re dealing with primitive types (typeof works great)
Type predicates earn their keep when you’re checking the same type distinction multiple times across your codebase, or when the type check involves multiple properties.
The Bottom Line
Type predicates turn TypeScript from a passive type checker into an active partner in your code. They eliminate the disconnect between what you know about your data and what TypeScript knows. Used well, they make your code both safer and more readable—without the verbosity of constant type assertions or the fragility of runtime casts.