← Back to Blog

Template Strings Are (Almost) Never Falsy

2026-10-06·4 min read
JavaScriptTypeScriptReactBest Practices

A template literal always produces a string. The only falsy string is the empty string "". So unless every piece of the template collapses to nothing, the result is truthy, no matter how broken the values inside it are.

`${undefined}`; // "undefined"  -> truthy
`${null}`; // "null"       -> truthy
`${false}`; // "false"      -> truthy
`${0}`; // "0"          -> truthy
`${NaN}`; // "NaN"        -> truthy
`${""}`; // ""           -> falsy (the only way out)

Every value gets converted with String(value) first. undefined doesn't disappear, it becomes nine letters. That's the whole bug, and it shows up in a few predictable places.


1. Guard clauses that never fire

// Bad: url is always a non-empty string
function loadProfile(userId) {
  const url = `/api/users/${userId}`;
  if (!url) return;
 
  return fetch(url); // fetches /api/users/undefined
}

The guard looks like it protects against a missing userId, but it's checking the wrong thing. By the time the check runs, userId has already been flattened into the string.

// Better: validate the input, then build the string
function loadProfile(userId) {
  if (!userId) return;
 
  return fetch(`/api/users/${userId}`);
}

Check the value, not the string you built from it.


2. || fallbacks that never fall back

// Bad: never reaches "Anonymous"
const displayName = `${user.firstName} ${user.lastName}` || "Anonymous";

With both names missing, displayName is "undefined undefined". With empty strings it's " " (one space), which is still truthy. The fallback is dead code.

// Better: drop the missing parts first, then fall back
const displayName =
  [user.firstName, user.lastName].filter(Boolean).join(" ") || "Anonymous";

filter(Boolean) removes undefined, null and "" before joining, so an all-empty list joins to "" and the || finally has something falsy to act on.


3. Leaking "false" and "undefined" into the UI

This one is common in React class names:

// Bad: renders class="btn false" when not active
<button className={`btn ${isActive && "btn-active"}`}>Save</button>

isActive && "btn-active" evaluates to false, and the template turns that into the literal class false. Same thing with optional values in visible text:

// Bad: shows "Hello, undefined!" while the user is loading
<Text>{`Hello, ${user?.name}!`}</Text>
// Better: decide the value before interpolating
<button className={`btn ${isActive ? "btn-active" : ""}`}>Save</button>
 
<Text>{user?.name ? `Hello, ${user.name}!` : "Hello!"}</Text>

A ternary always gives you control over both branches. && only gives you control over one, and the other one ends up in your markup.


4. Conditional rendering on a template

// Bad: badge always renders, even for 0 or undefined
{
  `${unreadCount}` && <Badge count={unreadCount} />;
}

Someone wrapped unreadCount in a template, often to avoid React Native's "Text strings must be rendered within a <Text> component" error when unreadCount is 0. It does avoid that error, by making the condition always true.

// Better: a real boolean condition
{
  unreadCount > 0 && <Badge count={unreadCount} />;
}

Let the tools catch it

You don't have to spot all of these by eye.

  • ESLint no-constant-binary-expression flags `Hi ${name}` || "Guest", because a template with literal text in it is always truthy.
  • ESLint no-constant-condition flags if (`/api/${id}`) for the same reason.
  • typescript-eslint restrict-template-expressions stops undefined, null or boolean values from being interpolated in the first place, which is where "undefined" and "false" come from.
// eslint.config.mjs
rules: {
  "no-constant-binary-expression": "error",
  "no-constant-condition": "error",
  "@typescript-eslint/restrict-template-expressions": "error",
}

The first two are in eslint:recommended. The third is in typescript-eslint's recommended-type-checked config.


Quick reference

What you wroteWhat you get when the value is missingDo this instead
if (!`/api/${id}`)Check never firesif (!id) before building the string
`${a} ${b}` || "Fallback""undefined undefined"[a, b].filter(Boolean).join(" ") || "Fallback"
`btn ${active && "on"}`"btn false"`btn ${active ? "on" : ""}`
`Hello, ${user?.name}`"Hello, undefined"Ternary, or ${user?.name ?? "there"}
{`${count}` && <Badge />}Badge always renders{count > 0 && <Badge />}

The rule of thumb: once a value is inside a template literal, it's too late to ask whether it was there. Ask before you interpolate.