Type Guards are mechanisms that allow you to refine the type of a variable within a code block based on some checks (for example, using typeof, instanceof, or special functions returning expressions of the form param is SomeType).
Main advantage — is safety and the elimination of runtime errors through type checking at compile-time.
Example:
interface Fish { swim: () => void } interface Bird { fly: () => void } function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim !== undefined; } function move(pet: Fish | Bird) { if (isFish(pet)) { pet.swim(); } else { pet.fly(); } }
Here, the function isFish is a custom type guard.
Nuances:
Question: "Will the TypeScript compiler always rely only on the returned value of the guard function, or does it use some other analysis inside the function?"
Answer:
The TypeScript compiler relies solely on the signature of the returned value param is Type. What happens inside the guard function is not analyzed for correctness of implementation.
Example (dangerous error!):
function isString(x: any): x is string { return true; } // The compiler will assume it’s always a string, although this is not the case: if (isString(123)) { // here x is of type string, but in fact it is a number }
Story
In a project with shared DTOs between the front-end and backend, a strict check was forgotten inside the custom type guard. As a result, some data was incorrectly perceived as the required type, leading to client-side crashes when trying to use a missing property.
Story
A developer wrote a type guard relying on an optional field, but the data structure allowed for that field to be completely absent. As a result, the type switch-case lacked a branch, and the compiler did not issue warnings — exceptions arose at runtime.
Story
In one of the services, when transitioning to TypeScript, they relied solely on built-in type guards (typeof, instanceof). When the prototype of objects changed during execution, the checks became incorrect, causing hard-to-debug bugs in production.