You may remember to use the || operator with caution to set defaults. We'll see that && and other conditionals come with the same limitations. However, TypeScript and eslint can make this a lot safer for us.
TL;DR
-
&&can be used as non-null safe access -
||can be used fornull/undefinedfallback values - Danger: falsey values give false negatives and bypass checks wrongly
- Prefer
??(nullish coalescing) or enablestrict-boolean-expressionsto avoid the 0/empty-string
&& for non-null access
Returns the first falsey value encountered, or the last value if all are truthy.
// Equivalent if/else
function arrayLength(strings: string[] | undefined): number | undefined {
if (strings === undefined) return undefined;
else return strings.length;
}
// Terse &&
function arrayLength(strings: string[] | undefined): number | undefined {
return strings && strings.length;
}
On the right side of &&, TypeScript knows strings is not undefined as it has been narrowed to string[].
|| for fallback values
Returns the first truthy value encountered, or the last value if all are falsey.
// Equivalent if/else
function numberOrOne(n: number | undefined): number {
if (n === undefined) return 1;
else return n;
}
// Terse ||
function numberOrOne(n: number | undefined): number {
return n || 1;
}
The falsey trap
0 and '' are falsey (
and many others as well
Show archive.org snapshot
), so || treats them as "missing" even when they are valid values.
function numberOrOne(n: number | undefined): number {
return n || 1;
}
numberOrOne(3); // 3
numberOrOne(0); // 1, but should be 0!
The same trap applies to &&, the ternary operator ?: and any conditional.
TypeScript catches swapped operators
Accidentally using || instead of && (or vice versa) usually produces a type error:
function arrayLength(strings: string[] | undefined): number | undefined {
return strings || strings.length; // type error: 'strings' is possibly 'undefined'
}
TypeScript narrows strings to undefined on the right side of || (because that side runs when strings is falsey). So accessing .length there is an error.
Ternary
?:operatorThe ternary operator
?:allows type narrowing in both branches, just likeif/else.
How to fix it
Option 1: Use ?? (nullish coalescing)
Instead of ||, ?? only fallbacks on null and undefined. It does not on 0 or ''.
function numberOrOne(n: number | undefined): number {
return n ?? 1;
}
numberOrOne(0); // 0
numberOrOne(undefined); // 1
Option 2: Use explicit comparison
return n !== undefined ? n : 1;
Option 3: Enable strict-boolean-expressions (recommended)
The
@typescript-eslint/strict-boolean-expressions
Show archive.org snapshot
rule forbids non-boolean types in boolean positions, catching the entire class of ||/&& bugs at lint time.
// eslint.config.mjs
export default tseslint.config({
rules: {
"@typescript-eslint/strict-boolean-expressions": "error"
}
});
The rule enforces explicit nullability checks on potentially unsafe types:
let num: number | undefined = 0;
if (num) { console.log('num is defined'); } // Unsafe because num could be 0
if (num != null) { console.log('num is defined'); } // This is safe
So with the rule enabled, the unsafe usage of these operators would error:
return n || 1;
return strings && strings.length;
Now when we would use strings !== null && strings.length or n !== null || 1 it significantly changes the returned type from number | undefined to number | boolean.
Because of this, these are the actual rewrites we want:
// Thus, || replaced with ??
return n ?? 1;
// && replaced with optional chaining
return strings?.length;
// or explicit null check with ternary
return strings !== null ? strings.length : undefined;