TypeScript: Enable strict-boolean-expressions for Safe Nullish Checks

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

  • Conditionals like using &&, ||, if else, ?: can be used for safe nullish checks
  • Danger: falsey values give false negatives and will bypass checks wrongly
  • Remember to always be explicit (like using ??) or enable strict-boolean-expressions to avoid the falsey trap

&& 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 ?: operator

The ternary operator ?: allows type narrowing in both branches, just like if/else.

How to fix it

Option 1: Remember to be explicit and to use safe variants

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

You can also replace && by using using optional chaining with .? (e.g. strings?.length).

Additionally you'll be have to explicit about falsey values and what you're actually checking for:

return n !== undefined ? n : 1;

Option 2: 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;
Felix Eschey