skip to content

Why is `if (items)` a broken way to check that a JavaScript array has elements, and what does ToBoolean do with objects in general?

level: middleimportance: should knowfreq 45%

answer

  1. the conversion never looks inside
  2. one row in the table covers every object
  3. an empty container is not an empty value
  4. the check you wrote is about null, not count
  5. ask length, size, or Object.keys

basics

~20 s

ToBoolean classifies every object as true without inspecting it, so an empty array is truthy. if (items) only proves the variable is not null or undefined; emptiness needs items.length === 0, or .size for a Map or Set.

solid answer

~40 s

ToBoolean has one rule for objects: they are all truthy. It never reads a property, never calls `valueOf` or `toString`, and never looks at length or contents. So `[]`, `{}`, an empty `Map` and even `new Boolean(false)` all pass an `if`. That makes `if (items)` a *nullish* check dressed up as an emptiness check — it tells you the variable holds an object rather than `null` or `undefined`, and nothing more. To test emptiness you have to ask the container: `items.length === 0` for arrays and strings, `set.size === 0` for `Set` and `Map`, `Object.keys(obj).length === 0` for a plain object. If both questions matter, ask both — `if (items?.length)` covers missing-or-empty in one expression, which is usually the condition people actually wanted.

code

javascript · 14 lines
javascript
const items = [];

if (items) console.log('truthy');       // logs — an empty array is still an object
console.log(Boolean(new Map()));        // true
console.log(Boolean(new Boolean(false))); // true — the wrapper is an object

// ask the container instead
console.log(items.length === 0);        // true
console.log(new Map().size === 0);      // true
console.log(Object.keys({}).length === 0); // true

// missing-or-empty in one expression
const maybe = undefined;
console.log(!maybe?.length, !items?.length); // true true

go deeper

for a junior

Remember that every object is truthy, so if (arr) passes even for an empty array, and use arr.length === 0 when you mean empty.

for a middle

Explain that ToBoolean maps the whole Object type to true without invoking ToPrimitive or reading any property, which is why it never throws and why boxed primitives are truthy.

for a senior

Show that you separate the two states in real code: nullish means the data never arrived, empty means it arrived with nothing, and a UI or a retry policy usually needs to treat those differently.

for a principal

Push it up to contract design — an API that can return null, [] or a populated array forces every consumer to write both checks, so decide at the interface whether 'no results' is representable as anything other than an empty collection.

## The rule for objects is unconditional The specification's ToBoolean operation is a table lookup on the value's *type*, not its contents. For the Object type there is a single row, and it says `true`. There is no escape hatch: no `Symbol.toPrimitive`, no `valueOf`, no `toString`, no length property is consulted. This is different from almost every other coercion in JavaScript — `+obj`, `` `${obj}` `` and `obj == 1` all invoke ToPrimitive and can run user code — and it is why truthiness is the one conversion that never throws and never has side effects. ```js Boolean([]); // true Boolean({}); // true Boolean(new Map()); // true Boolean(() => {}); // true Boolean(new Boolean(false)); // true — the wrapper is an object Boolean(new Number(0)); // true — likewise ``` The last two are the sharpest illustration. `new Boolean(false)` wraps the falsy primitive `false` in an object, and the object is truthy — so `if (new Boolean(false))` runs its block. That, more than anything, is why the wrapper constructors should never be called with `new`. ## What `if (items)` actually asks Since every object is truthy, the only way `if (items)` takes the false branch is if `items` holds a falsy **primitive** — in practice `undefined` (never assigned, missing property, function returned nothing) or `null`. So the check means "`items` is not nullish", plus an accidental extra branch if `items` could ever be `0` or `''`. That is a perfectly reasonable check, and often it is the one you want after an API call that may return nothing. The defect is only in *believing* it also covers emptiness. Code like ```js if (results) { render(results); } else { showEmptyState(); } ``` never shows the empty state for `results = []`; it renders an empty list instead. ## Asking the container instead Emptiness is a per-type question, so use the type's own answer: ```js arr.length === 0; // Array (and String) map.size === 0; // Map, Set, WeakMap has no size at all Object.keys(obj).length === 0; // plain object, own enumerable string keys ``` For plain objects, be aware that `Object.keys` reports own enumerable string-keyed properties only — symbol keys and inherited properties are not counted — so "empty" means "empty by that definition", which is usually what you mean but is worth saying out loud. Combining the two questions is the common real requirement: ```js if (!items?.length) showEmptyState(); // covers null, undefined, and [] ``` `?.` yields `undefined` when `items` is nullish, `undefined` is falsy, and `0` is falsy — so the single expression means "missing or empty". It is compact, but it is also implicit; in code where the two cases deserve different handling (an error state versus an empty state) write them as two branches. ## Related traps in the same family - **Boxed primitives.** Anything produced by `new Number(...)`, `new String(...)` or `new Boolean(...)` is an object and therefore truthy. Called *without* `new`, those same functions return primitives and behave normally. - **`document.all`** is the one specified falsy object, a browser legacy exception that no code you write can reproduce. - **Arrays and loose comparison.** Trying to fix the check with `items == false` looks tempting because `[] == false` really is `true`, but that comparison coerces both sides to numbers, so `[0] == false` is also `true` — it is not an emptiness test at all. - **`items === []`** is always `false`, since it compares two distinct object references rather than contents. ## What a strong answer sounds like State the mechanism first — ToBoolean maps the entire Object type to `true` with no inspection — then draw the two conclusions: `if (obj)` is a nullish check, and emptiness must come from the container's own `length`, `size` or key count. Finish with the practical form you would actually write, `if (!items?.length)`, and note when you would split it into two explicit branches instead.

  • Someone suggests fixing the check with `items == false`, pointing out that `[] == false` is true. Is that a valid emptiness test?
    No. That comparison coerces both operands to numbers, and an array coerces via its string form, so `[0] == false` and `[''] == false` are also `true` while `[1] == false` is `false`. It is testing 'does this array stringify to something that numbers to zero', which coincides with emptiness only by accident. Use `items.length === 0`.
  • How would you write a check that distinguishes 'the request failed' from 'the request succeeded with no results'?
    Two explicit branches, because they are two different states: `if (items == null) showError()` then `else if (items.length === 0) showEmptyState()` then `else render(items)`. The compact `!items?.length` form collapses both into one, which is fine when the UI is identical but hides a real distinction when it is not.

saying these in an interview costs you the question

  • Assumes an empty array or empty object is falsy
  • Thinks ToBoolean checks length or key count on objects
  • Says `new Boolean(false)` is falsy
  • Uses `items === []` to test for an empty array
  • Believes `if (obj)` proves the object has usable data

context