Truthiness narrowing
Narrows a value by testing it for truthiness, removing null, undefined and the other falsy types.
if (value) { /* value: NonNullable<T> minus falsy literals */ }- Added in TypeScript 2.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$value | T | The tested expression. Positions that coerce to boolean all narrow: if, while, the ternary condition, && and ||, and the operand of !. |
Return value
booleanNothing directly. In the true branch the compiler removes null, undefined and any falsy literal types; the false branch keeps only the types a falsy value could have had.
Errors
Nothing. Truthiness testing never throws, which is why it silently hides bugs rather than reporting them.
Changelog
- PHP 2.0Control flow based type analysis and non-null / non-undefined type guards introduced under --strictNullChecks; a truthiness check removes null and undefined from a union.
- PHP 4.4A truthiness check stored in a const is tracked, so `const ok = a && b; if (ok) { ... }` narrows both a and b.
Anything that coerces to boolean narrows: if, while, the ternary condition, &&, || and !. In the true branch TypeScript removes every type whose values are all falsy — null, undefined, and literal types like 0, "", false and 0n.
When to reach for it
- Guarding an object or array reference before dereferencing it, where no legitimate value is falsy.
- As the leading half of a compound guard:
if (strs && typeof strs === "object"), where the truthiness test is what removesnullbefore thetypeofcheck that would otherwise let it through.
What trips people up
Empty string and zero are falsy. A guard written to mean "was this provided?" also rejects "" and 0, which are usually valid inputs. if (!name) return "stranger" throws away a deliberately blank name; count ? ... : "none" reports zero items as missing. Because the compiler agrees the branch is unreachable for those values, you get no warning at all — this is the most common source of quiet bugs in narrowing code.
When you mean "present", write value !== undefined, value != null, or value ?? fallback. Save truthiness for values where falsy really is invalid.
Example
When to use it
- Bailing out early when an object reference may be null before reading its fields
- Removing null before a typeof "object" check that would otherwise still include it
- Auditing an existing codebase for `if (count)` guards that mishandle zero
Frequently asked questions
Why is my zero being treated as missing?
Because 0 is falsy, so `if (count)` and `count || fallback` both skip it. Use `count !== undefined`, `count != null`, or the `??` operator, which only substitutes for null and undefined.
Which values are falsy in JavaScript?
false, 0, -0, 0n, "", null, undefined and NaN. Everything else, including empty arrays and empty objects, is truthy.
Does a truthiness check remove null and undefined from the type?
Yes, under strictNullChecks the true branch drops both, plus any all-falsy literal types like 0 or "". That is precisely why it over-narrows for optional numbers and strings.
More examples
Truthiness as the null-remover in a compound guard
Prints [ 'a', 'b' ], [ 'solo' ], [], [ '' ]. Note the last line: the empty string still reaches the string branch, because the truthiness test only guards the object branch. Wrapping the whole body in `if (strs)` instead would have silently dropped it.
function printAll(strs: string | string[] | null): string[] {
const out: string[] = [];
if (strs && typeof strs === "object") {
// strs: string[] — the leading `strs &&` is what removed null,
// because typeof null === "object" would have kept it.
for (const s of strs) out.push(s);
} else if (typeof strs === "string") {
out.push(strs);
}
return out;
}
console.log(printAll(["a", "b"]));
console.log(printAll("solo"));
console.log(printAll(null));
console.log(printAll(""));?? and ?. do not use truthiness
Prints "|| 3 ?? 0 label " then "|| 5 ?? 5 label GO" then "|| 3 ?? 3 label (none)". The first line is the whole lesson: `||` treats 0 as absent, `??` does not, and `?.` short-circuits only on null and undefined — an empty string still gets .toUpperCase() called on it.
type Settings = { retries?: number; label?: string };
function summarise(s: Settings): string {
const retries = s.retries || 3; // 0 becomes 3 — wrong
const safe = s.retries ?? 3; // only undefined/null becomes 3
const upper = s.label?.toUpperCase() ?? "(none)";
return `|| ${retries} ?? ${safe} label ${upper}`;
}
console.log(summarise({ retries: 0, label: "" }));
console.log(summarise({ retries: 5, label: "go" }));
console.log(summarise({}));
Discussion