Null checks and equality
Let's go back to the sorting belt. Sometimes a tray reaches the arch empty. Before deciding whether it holds an envelope or a can, the worker checks whether there is anything in the tray at all. An empty tray is set aside, and only real parcels ride on down the belt.
In a program, the empty tray is null or undefined. You already know that a value of type string | null does not let you call a string method until null is ruled out:
function greet(nick: string | null) {
console.log("Hi, " + nick.toUpperCase()); // ✗
}error TS18047: 'nick' is possibly 'null'.For null and undefined you do not need a typeof check. TypeScript narrows the type after two other checks as well: a truthiness condition and a comparison.
The truthiness check
The condition if (nick) lets only truthy values into the branch. In JavaScript, null and undefined are falsy, so tsc removes them from the type inside the if:
function greet(nick: string | null) {
if (nick) {
console.log("Hi, " + nick.toUpperCase());
} else {
console.log("Hi, guest");
}
}
greet("ann"); // Hi, ANN
greet(null); // Hi, guest
greet(""); // Hi, guestLet's walk through the example line by line:
- Inside the
if,nickhas the typestring:nullnever gets here, andtoUpperCasecompiles. greet("ann")printsHi, ANN.greet(null)goes to theelseand printsHi, guest.greet("")printsHi, guesttoo: an empty string is falsy in JavaScript.
The last call matters. In the else, nick has the type string | null, not null: the empty string ends up there as well. A truthiness condition cuts off every falsy JavaScript value at once:
nullandundefined;false;0,0nandNaN;- the empty string
"".
Here the tray picture parts ways with TypeScript. An empty string and zero are not empty trays but real parcels, yet the truthiness check sets them aside together with the empty trays.
Comparing with = and !
When only null must be cut off, you compare with it explicitly. TypeScript understands both === and !==:
function badge(unread: number | null) {
if (unread !== null) {
return "unread: " + unread;
}
return "loading";
}
function same(a: string | number, b: string | boolean) {
if (a === b) {
return a.toUpperCase();
}
return "-";
}
console.log(badge(0), same("ok", "ok")); // unread: 0 OKThe program prints unread: 0 OK. Let's go through both functions.
Comparing with null
unread !== null removes exactly null from the type. Inside the if, unread has the type number, and zero stays a real number, so badge(0) returns unread: 0. After the return, unread is left with the type null.
For undefined you write the same: x !== undefined. But typeof x === "object" does not work for this: in JavaScript typeof null gives "object" too, and tsc remembers it, so null stays in that branch.
Comparing two values
You can also compare two values with union types. If a === b is true, a and b hold the same value, so its type is one that appears in both unions:
ahas the variantsstringandnumber;bhas the variantsstringandboolean;- they share only one variant—
string.
So inside the if, tsc treats both a and b as strings, and a.toUpperCase() compiles.
Common mistake
A number is checked for truthiness to cut off null:
function badge(unread: number | null) {
if (unread) { // ✗
return "unread: " + unread;
}
return "loading";
}
console.log(badge(0));tsc is silent: inside the if, unread has the type number, and everything is legal. But the program prints loading, even though the message count is known—it is zero. Zero is falsy, and the condition sent it to the same branch as null. When 0 or an empty string is a normal value, check unread !== null, as in the example above. Interviewers like to test this trap.
Key points
if (x)removesnullandundefinedfrom the type, but it also cuts off the other falsy values:0,"",NaN,false.x !== nullremoves exactlynullfrom the type, and zero and the empty string stay in the branch.- If
a === b, inside theifboth values have the type their unions share. - tsc will not warn you that
if (unread)sent zero to the "no value" branch: this is a logic mistake, not a type mistake.