CodeStaminaOpen preview

Data boundaries · TypeScript · 8 min

Missing, empty, zero: three states one falsy check collapses

if (value) asks whether a value is truthy. Most of the time you meant to ask whether it was provided.

Train this

The problem

A product listing accepts optional filters. Each filter is applied only when the caller supplied it, so the natural implementation guards each one with a truthiness check.

It works in every manual test, because every manual test uses a non-empty search term and a sensible price.

Optional filters guarded by truthiness
interface Item { name: string; price: number }
interface Filters { search?: string; maxPrice?: number }

function apply(items: Item[], filters: Filters): Item[] {
  let result = items;
  if (filters.search) {
    result = result.filter(item => item.name.includes(filters.search!));
  }
  if (filters.maxPrice) {
    result = result.filter(item => item.price <= filters.maxPrice!);
  }
  return result;
}

const catalog = [{ name: 'Sticker', price: 0 }, { name: 'Mug', price: 12 }];
console.log(apply(catalog, { maxPrice: 0 }));

PREDICT FIRST

A shopper filters for free items only — maxPrice is 0. What comes back?

Commit to an answer before you read on. Being wrong here is the part that sticks.

Why it behaves that way

The guard conflates two different questions. "Did the caller supply a maximum price?" is a question about presence. "Is the maximum price non-zero?" is a question about value. Truthiness answers the second one and gets used for the first.

The same collapse hits search: "". An empty string usually means the user cleared the box, which is genuinely "no filter" — but only by accident, and the code no longer distinguishes an intentional empty search from an omitted one.

Once you write the presence check explicitly, the remaining decisions become visible and you have to make them on purpose: is an empty search term a filter that matches everything, or a filter that matches nothing? Is null from a JSON payload the same as an absent key, or does it mean "clear this value"?

This is why ?? exists alongside ||. port ?? 8080 falls back only when port is null or undefined; port || 8080 also rewrites a deliberate 0.

Presence checked separately from value
function apply(items: Item[], filters: Filters): Item[] {
  let result = items;

  const { search, maxPrice } = filters;
  // An empty search box clears the filter; a blank-but-present term is a
  // decision, not an accident. Write it down.
  if (search !== undefined && search.trim() !== '') {
    result = result.filter(item => item.name.includes(search));
  }
  if (maxPrice !== undefined) {
    result = result.filter(item => item.price <= maxPrice);
  }
  return result;
}

What the change buys you

BehaviourBeforeAfter
maxPrice: 0Filter skipped, whole catalog returnedOnly free items returned
maxPrice omittedFilter skippedFilter skipped
search: ""Skipped by accidentSkipped by a documented rule
The non-null assertionsTwo ! needed inside the callbacksNone — narrowing survives destructuring

Variations worth trying

  1. Do the same audit on ?? versus || for numeric configuration: timeouts, retry counts, page sizes and rate limits all have meaningful zeroes.
  2. Give a PATCH endpoint three-state semantics: key absent means leave unchanged, key present with null means clear, key present with a value means set. A truthiness check cannot express that at all.
  3. Try the empty-array case: does tags: [] mean "no tag filter" or "items with no tags"? Both are defensible; only one is written down.
  4. Turn on strictNullChecks in a codebase that leans on truthiness and count how many of the resulting errors are real bugs rather than noise.

TRAIN IT

Fix a filter that ignores zero

A boundary workout: decide what absent, empty and zero each mean, then make the tests that cover all three pass.

Open the workout

JOIN THE WAITLIST

Help shape what we build next.

Tell us where you want to start and what you want to practise. We’ll use your answers to understand demand and email you about relevant program availability.

What would you like to practise?

We store your email and answers for this waitlist. No campaign is being sent now. Contact mo@codestamina.com to ask us to remove your entry.