A type guard function's return type is written value is Type instead of a plain boolean — this special return type, called a type predicate, tells TypeScript that a true return narrows the checked value to Type everywhere the function is used as a condition. This extends narrowing beyond what typeof/instanceof/in can express to any arbitrarily complex runtime check.
Writing a custom type guard
The pet is Fish return type is what makes this special — a plain : boolean return would work at runtime identically, but wouldn't narrow anything for the type-checker.
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function isFish(pet: Fish | Bird): pet is Fish { // the type predicate
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // narrowed to Fish, thanks to the type guard
} else {
pet.fly(); // narrowed to Bird
}
}Type guards with arrays: filtering out undefined
For a long time, .filter(x => x !== undefined) narrowed the runtime values correctly but TypeScript's own inference still left the array type as (T | undefined)[], so you had to write an explicit type guard like isDefined below to get a properly narrowed T[] back. Since TypeScript 5.5, the compiler can infer a type predicate straight from a simple filter callback's body — x => x !== undefined included — so items.filter(x => x !== undefined) now infers as string[] on its own, no named type guard required. An explicit guard like isDefined is still worth writing when you want to reuse the same check in several places or the condition is too complex for the compiler to infer automatically.
const items: (string | undefined)[] = ["a", undefined, "b", undefined];
// Since TS 5.5, the compiler infers this filter callback as a type predicate on its own:
const inferredFiltered: string[] = items.filter(x => x !== undefined);
function isDefined<T>(value: T | undefined): value is T {
return value !== undefined;
}
const filtered: string[] = items.filter(isDefined);
// a named, reusable type guard — still useful even though it's no longer required hereType guards for validating unknown data
This is one of the most practical real-world uses — safely narrowing genuinely unknown external data (a JSON API response, for instance) into a specific expected shape, with the check written once and reused everywhere that data is handled.
interface User { name: string; age: number; }
function isUser(data: unknown): data is User {
return (
typeof data === "object" && data !== null &&
"name" in data && "age" in data
);
}
const response: unknown = fetchApiResponse();
if (isUser(response)) {
console.log(response.name); // safely narrowed to User
}