TypeScript Logical Operator Narrowing (`&&`, `||`, `??`)

Posted . Visible to the public.

You may remember to use the || operator with caution to set defaults. We'll see that && comes with the same limitations. However, TypeScript and eslint can make their usage a lot safer for us.

TL;DR

  • && returns the first falsey value, or the second value if both are truthy
  • || returns the first truthy value, or the second value if both are falsey
  • Both operators narrow types on each side, just like if/else
  • Danger: 0, '', and NaN are falsey
  • Prefer ?? (nullish coalescing) or enable strict-boolean-expressions to avoid the 0/empty-string

&& as short-circuit for nullable 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[].

|| as short-circuit 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 0 / empty-string bug

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 && when a valid 0 or '' would short-circuit prematurely.

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

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 ?: actually also allows us to narrow the type and does not suffer from the falsy bugs.

Profile picture of Felix Eschey
Felix Eschey
Last edit
Felix Eschey
License
Source code in this card is licensed under the MIT License.
Posted by Felix Eschey to makandra dev (2026-03-02 21:08)