Truthiness Narrowing
JavaScript coerces values to booleans in if conditions, &&, ||, and ternaries, and TypeScript uses these coercions to narrow types. A truthy check can remove null, undefined, 0, "", NaN, and false from a union — which is powerful but also the source of classic bugs when a valid 0 or empty string is treated as “missing.” This page covers what counts as falsy, how &&/|| narrow, the optional-chaining and nullish-coalescing operators, and how to avoid the zero/empty-string trap.
The falsy values
A truthiness check narrows out every falsy value at once. There are exactly seven falsy values in JavaScript, and TypeScript knows each one.
| Value | Falsy? |
|---|---|
false | yes |
0, -0, 0n | yes |
"" (empty string) | yes |
null | yes |
undefined | yes |
NaN | yes |
| everything else | no (truthy) |
function greet(name: string | null | undefined): string {
if (name) {
// name: string (and non-empty at runtime)
return `Hello, ${name}`;
}
return "Hello, stranger";
}
Narrowing with && and ||
Logical operators narrow within the expression itself. In a && b, the right side only evaluates when a is truthy, so a is narrowed there. In a || b, the right side runs when a is falsy.
function firstChar(text: string | undefined): string {
// text is narrowed to string before .charAt runs
return (text && text.charAt(0)) || "?";
}
This guard-and-default style is concise, but for nullable-only checks the optional chaining and nullish coalescing operators express intent more precisely.
Optional chaining and nullish coalescing
?. short-circuits to undefined when the left side is null or undefined, and ?? supplies a fallback only for null/undefined — not for other falsy values. Together they avoid the truthiness trap.
interface Config {
timeout?: number;
}
function getTimeout(config: Config): number {
// ?? keeps a valid 0; || would wrongly replace it
return config.timeout ?? 5000;
}
If timeout is 0, ?? returns 0, whereas config.timeout || 5000 would incorrectly return 5000 because 0 is falsy.
Reach for
??instead of||whenever0,"", orfalseare legitimate values.||narrows on truthiness,??narrows only on nullishness.
The zero and empty-string trap
The most common truthiness bug is using a value’s truthiness as a proxy for “is it present?” when 0 or "" are valid inputs. Check for null/undefined explicitly instead.
function render(count: number | undefined): string {
// ❌ Bug: a real count of 0 is treated as missing
if (count) {
return `${count} items`;
}
return "no data";
// ✅ Fix: check for undefined directly
// if (count !== undefined) return `${count} items`;
}
The first version reports “no data” for count === 0. Using count !== undefined (or count == null inverted) narrows out only the nullish case while keeping 0.
Truthiness on objects and arrays
Objects and arrays are always truthy — even an empty array [] or empty object {}. A truthiness check therefore only narrows out null/undefined for object-typed values, never emptiness.
function hasItems(list: string[] | null): boolean {
if (list) {
// list: string[] — but could still be []
return list.length > 0;
}
return false;
}
To detect an empty array you must inspect .length; truthiness alone will not do it.
Best Practices
- Use truthiness checks to strip
null/undefinedfrom non-numeric, non-string unions. - Prefer
??over||whenever0,"", orfalseare valid values. - Use
?.to safely access members of possibly-nullish objects. - Check
!== undefinedor== nullexplicitly when a falsy value is meaningful data. - Remember every object and array is truthy; test
.lengthorObject.keysfor emptiness. - Keep guard-and-default
&&/||chains short and readable, or refactor to explicit checks.