Background: TypeScript expands regular type checks by allowing developers to create their own functions – type guards – that check whether an object conforms to a certain type. This is necessary for working with union types, dynamic structures, and APIs where the type of a value can vary.
The common issue is that standard type checks using typeof and instanceof are limited to primitives and classes, whereas structures or complex types cannot be defined without this. You need to explicitly inform the compiler when the value is safe to narrow down to the desired type.
The solution is to write guard functions of the form function isCat(obj: Animal): obj is Cat {...}, where the key point is the type predicate in the function's return type.
Code example:
interface Dog { bark: () => void; } interface Cat { meow: () => void; } type Pet = Dog | Cat; function isDog(pet: Pet): pet is Dog { return (pet as Dog).bark !== undefined; } function makeSound(pet: Pet) { if (isDog(pet)) { pet.bark(); } else { pet.meow(); } }
Key features:
Is it enough to return true/false in a type guard function for the compiler to narrow the type?
No. It is important to explicitly specify the return type as a type predicate (for example, pet is Dog), otherwise TypeScript will not automatically narrow the type of the value, even if the function only returns true or false.
Can a type guard be used inside a callback (e.g., in filter), and will the narrowing work correctly?
Yes, if the type guard is correctly annotated, the compiler will narrow the type of the array elements after filter and within forEach/callback functions. However, if the annotation is missing or incorrectly written, the result will have a union type instead of a refined type.
const pets: Pet[] = [...]; const dogs = pets.filter(isDog); // TypeScript knows that dogs: Dog[]
How do user-defined type guards differ from regular type checks using typeof, instanceof?
Type guard functions allow checking any structure, describing checks of any complexity, operating with interfaces without classes, rather than just basic types and classes.
A function filters users, creating a type guard without a type predicate:
function isValidAdmin(user: any): boolean { return user.isAdmin === true; } const admins = users.filter(isValidAdmin); // admins: any[]
Pros:
Cons:
A function filters with a correct type predicate:
interface Admin { name: string; isAdmin: true; } function isAdmin(user: any): user is Admin { return user && user.isAdmin === true; } const admins = users.filter(isAdmin); // admins: Admin[]
Pros:
Cons: