Background of the question:
With the development of TypeScript, the need arose to reliably define a narrower type of a variable in logical branches. Classic type checks (via typeof or instanceof) are not always sufficient, especially if the object has a complex structure or hierarchy. To enhance data safety and convenience, TypeScript implemented a mechanism for type predicates to create custom type guards.
The problem:
Standard type checks within functions do not give the compiler information about the type of the variable in subsequent code when only the result true/false is used. The compiler does not "understand" what exactly was checked. This leads to implicit runtime errors when we mistakenly access non-existent properties.
The solution:
Type predicates using a type substitution of 'param is Type' allow the compiler to understand that with this parameter after the check, we can work as with a specific type. Such functions enhance type safety and extend the narrowing system for any complex tasks.
Code example:
interface Bird { fly(): void; feathers: boolean; } interface Fish { swim(): void; fins: number; } function isBird(animal: Bird | Fish): animal is Bird { return (animal as Bird).fly !== undefined; } const pet: Bird | Fish = ...; if (isBird(pet)) { pet.fly(); // OK: pet is now Bird } else { pet.swim(); // OK: pet is now Fish }
Key features:
Can a type guard function work if the return type 'param is Type' is not explicitly specified in the signature?
No, if 'param is Type' is not explicitly specified in the signature, TypeScript will not be able to narrow the type in the branches of the code, regardless of whether the return value is true/false. The compiler will not understand that the parameter can be used as a specific type.
Code example:
function isFish(animal: Fish | Bird): boolean { return (animal as Fish).swim !== undefined; } // Does it work? if (isFish(pet)) { pet.swim(); // Error: Property 'swim' does not exist }
Can type predicates be used to check primitive values like strings or numbers?
Yes, they can, but typeof is more commonly used, and such guards become redundant. However, there is nothing stopping you from implementing a custom guard:
function isString(x: unknown): x is string { return typeof x === "string"; }
Does the type guard function provide strict protection against type errors at compile-time?
Not completely. TypeScript relies on the implementation of the function itself and cannot verify the correctness of the logic within it. If you incorrectly implement the check, the compiler will not understand the error, and problems will arise at runtime.
function isFish(animal: Fish | Bird): animal is Fish { // Incorrect: always returns true return true; }
Negative case A developer implemented a predicate function but made an error in the structure check, causing the function to always return true. The code passed compilation, but at runtime, a non-existing method was called.
Pros:
Cons:
Positive case Type predicate functions are correctly implemented, unit tested on edge cases and erroneous data.
Pros:
Cons: