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.
// 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.
|| 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.
"false" and "undefined" into the UIThis 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.
// 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} />;
}You don't have to spot all of these by eye.
no-constant-binary-expression flags `Hi ${name}` || "Guest", because a template with literal text in it is always truthy.no-constant-condition flags if (`/api/${id}`) for the same reason.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.
| What you wrote | What you get when the value is missing | Do this instead |
|---|---|---|
if (!`/api/${id}`) | Check never fires | if (!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.