Skip to Content
Course content

135: Truthiness Narrowing

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've already seen how we can use typeof or instanceof to tell TypeScript what a variable is. But in the real world, we spend a huge amount of our time just checking if something exists. This is where truthiness narrowing comes in.

TypeScript is smart enough to know that if you put a variable into an if statement, you're essentially asking: "Is this value truthy?" If the check passes, TypeScript automatically narrows the type by removing all the falsy possibilities—like null, undefined, 0, NaN, and empty strings.

Building a dynamic user greeting

Let's imagine we're building a small piece of a profile page. We have a user object where the displayName is optional. If it's there, we want to use it; otherwise, we'll fall back to "Guest".

type User = {
  displayName?: string;
  id: string;
};

function greetUser(user: User) {
  if (user.displayName) {
    // Inside this block, TypeScript knows displayName is definitely a string.
    // It has narrowed the type from 'string | undefined' to just 'string'.
    console.log(`Welcome back, ${user.displayName.toUpperCase()}!`);
  } else {
    console.log("Welcome, Guest!");
  }
}

This is the "magic" of truthiness. I didn't have to write if (user.displayName !== undefined). I just checked the value itself, and TypeScript followed my logic.

Where I usually trip up: The empty string trap

Here is where I've bitten myself more than once. Truthiness is convenient, but it's a blunt instrument. It doesn't just filter out null or undefined; it filters out everything falsy.

Let's say I want to allow users to have an empty string as their status, and I only want to show a "No status set" message if the value is actually null or undefined. Watch what happens when I try to use truthiness narrowing here:

function formatStatus(status: string | null) {
  if (status) {
    return `Status: ${status}`;
  }
  return "No status set";
}

console.log(formatStatus("Coding...")); // "Status: Coding..."
console.log(formatStatus(null));        // "No status set"
console.log(formatStatus(""));          // "No status set" <--- Wait, this is wrong!

I just realized my mistake. By using if (status), I accidentally treated the empty string "" as if it were null. In many apps, an empty string is a valid value (meaning the user explicitly cleared their status), while null means they've never touched it. My "convenient" narrowing just created a bug.

Switching to explicit nullish checks

When truthiness is too broad, I stop relying on it and move to a more surgical approach. If I only care about null and undefined, I'll check for those specifically, or use the nullish coalescing operator.

function formatStatusFixed(status: string | null) {
  // By checking explicitly against null, I leave the empty string alone.
  if (status !== null) {
    // Now status is narrowed to just 'string', including ""
    return `Status: ${status}`;
  }
  return "No status set";
}

console.log(formatStatusFixed("")); // "Status: " (Correct!)

The takeaway here is balance. Use truthiness narrowing when you genuinely want to exclude all falsy values. But the moment you're dealing with numbers (where 0 is a valid value) or strings (where "" is valid), be explicit. Your future self will thank you when you're debugging a weird edge case at 4 PM on a Friday.




📋 Practical Task

Fixing the Zero-Value Bug in a Scoreboard

You are building a game scoreboard. The currentScore can be a number or null (if the game hasn't started). The current implementation uses truthiness narrowing, which is causing a bug: when a player has a score of 0, the app incorrectly tells them they haven't started the game yet.

Fix the displayScore function so that it correctly displays "Score: 0" instead of "Game not started".

type GameState = {
  currentScore: number | null;
};

function displayScore(game: GameState) {
  // FIX THIS LINE:
  if (game.currentScore) {
    return `Score: ${game.currentScore}`;
  }
  return "Game not started";
}

const player1 = { currentScore: 10 };
const player2 = { currentScore: 0 };
const player3 = { currentScore: null };

console.log(displayScore(player1)); // Expected: "Score: 10"
console.log(displayScore(player2)); // Currently: "Game not started" | Expected: "Score: 0"
console.log(displayScore(player3)); // Expected: "Game not started"
Rating
0 0

There are no comments for now.

to be the first to leave a comment.