User-defined type guards
A complex check on the sorting belt is moved into a separate booth. The worker in the booth opens a parcel, looks inside and stamps it "courier". After that nobody opens the parcel again: it is routed by the stamp. The belt takes the stamp at its word.
In a program, such a booth is a check function. The "phone" in d check from the lesson on in is handy to move into a function: the rule is written once and can be called anywhere in the program. But if you declare that the function returns a boolean, the narrowing is lost:
type Courier = { phone: string };
type Pickup = { address: string };
function isCourier(d: Courier | Pickup): boolean {
return "phone" in d;
}
function contact(d: Courier | Pickup) {
if (isCourier(d)) {
return "call " + d.phone; // ✗
}
return "pick up";
}error TS2339: Property 'phone' does not exist on type 'Courier | Pickup'.
Property 'phone' does not exist on type 'Pickup'.For tsc, the result of isCourier(d) is an ordinary true or false. The boolean type does not show that true means "this is a courier", so in the branch d keeps the whole union.
Type predicates
To make a function narrow the type, you write a type predicate instead of boolean—the notation d is Courier. It reads: "if the function returned true, then d is a Courier":
type Courier = { phone: string };
type Pickup = { address: string };
function isCourier(d: Courier | Pickup): d is Courier {
return "phone" in d;
}
function contact(d: Courier | Pickup) {
if (isCourier(d)) {
return "call " + d.phone;
}
return "pick up at " + d.address;
}
console.log(contact({ address: "Main St 5" })); // pick up at Main St 5Let's walk through the example line by line:
d is Courierstands where the result type goes. When the program runs, the function returns an ordinarytrueorfalse.- To the left of
isis the name of the function's parameter: the predicate speaks aboutd. - Inside
if (isCourier(d)),dhas the typeCourier, as after the check"phone" in d. - After the
return,Pickupis left: the predicate also narrows the branch where the function returnedfalse. - The program prints
pick up at Main St 5.
This is the stamp from the booth. The function looks at the object once, and an if anywhere in the program narrows the type by its answer.
Here the stamp picture parts ways with TypeScript. A stamp stays on the parcel, while a predicate leaves nothing on the object. After compilation, d is Courier turns into an ordinary function that returns true or false.
When TypeScript infers the predicate
Since version 5.5, TypeScript can infer the predicate by itself. Remove : boolean from the first example, and the editor's hover over isCourier shows function isCourier(d: Courier | Pickup): d is Courier, and the narrowing works.
This does not always happen. The predicate is inferred for a function with no result type that has a single return returning the check itself. If the body has several returns, as in if ("phone" in d) { return true; } return false;, the result type is boolean again. In such cases, and when you want the promise to be visible in the function's declaration, you write the predicate explicitly.
Interviewers often put it this way: how do you write your own function that narrows a type? The answer: declare its result as an x is T predicate.
Common mistake
The body of the check function tests the wrong key:
type Courier = { phone: string };
type Pickup = { address: string };
function isCourier(d: Courier | Pickup): d is Courier {
return "address" in d; // ✗
}
function contact(d: Courier | Pickup) {
if (isCourier(d)) {
return "call " + d.phone.trim();
}
return "pick up at " + d.address;
}
console.log(contact({ address: "Main St 5" }));tsc is silent: it does not check the function's body against its predicate. The function returns true for a pickup, and tsc trusts the predicate and treats d as a courier. So it allows d.phone.trim(), even though a pickup has no phone. When the program runs, it crashes:
TypeError: Cannot read properties of undefined (reading 'trim')A predicate is a promise, and you have to check it yourself—the compiler will not. A wrong body narrows the type wrongly, and the mistake shows up only at run time.
Key points
- A function with the result type
booleandoes not narrow the type where it is called. - The predicate
d is Courierin place ofbooleannarrows the type: toCourierontrue, to the other members onfalse. - TypeScript 5.5 and later infers the predicate by itself when a function has no result type and its single
returnreturns the check itself. - tsc takes a predicate at its word: a wrong body narrows the type wrongly, and the mistake shows up at run time.