Skip to Content
Course content

136: Equality Narrowing

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

You've likely encountered situations where a variable could be one of a few different types, and you just want TypeScript to get out of your way so you can call a method on that object. When I first started with TypeScript, my instinct was to just tell the compiler, "Trust me, I know what this is." That's a dangerous habit that leads to runtime crashes that the type system was specifically designed to prevent.

Fighting the compiler with type assertions

Let's look at a common scenario: handling a user's subscription status. Suppose you have a variable that is either a string representing the plan name or null if they aren't subscribed. The naive approach is to use a type assertion (the as keyword) to force the type when you think you've already handled the logic elsewhere.

function getPlanDetails(status: string | null) {
  if (!status) {
    return "No active plan";
  }
  
  // Naive approach: forcing the type
  return (status as string).toUpperCase();
}

This works, but it's brittle. You're essentially bypassing the type checker. If someone changes the if (!status) check later to something less restrictive, or if the logic becomes more complex, that as string remains a blind spot. I've seen far too many production bugs where a developer "promised" the compiler a type was a string, but a null slipped through because the guard clause was slightly off. You're basically telling TypeScript to stop doing its job.

Letting the equality check do the heavy lifting

The better way is to use Equality Narrowing. TypeScript is smart enough to realize that if you've checked a variable against a specific value using === or !==, the type of that variable is narrowed within that block of code. You don't need to cast anything; the compiler just knows.

function getPlanDetails(status: string | null) {
  if (status === null) {
    return "No active plan";
  }
  
  // TypeScript now knows status MUST be a string here
  return status.toUpperCase();
}

Notice how much cleaner that is. By checking for null explicitly, the rest of the function treats status as a string. This isn't just about saving a few keystrokes; it's about creating a "proven" path. If you try to change the logic in a way that makes status potentially null again, TypeScript will immediately flag the toUpperCase() call as an error. That's the safety net we actually want.

Narrowing with literal types

This gets even more powerful when you're dealing with union types of strings (literals). If you're checking for a specific value, TypeScript narrows the type to that exact string literal, which is incredibly useful for exhaustive checks.

type UserRole = "admin" | "editor" | "viewer";

function getPermissions(role: UserRole) {
  if (role === "admin") {
    // In this block, role is specifically the type "admin"
    return ["read", "write", "delete"];
  }
  
  // Here, role is narrowed to "editor" | "viewer"
  return ["read"];
}

I always prefer this approach because it makes the code self-documenting. Anyone reading the code can see exactly which conditions lead to which types. You aren't guessing based on a cast; you're following the logical flow of the data.




📋 Practical Task

Refactoring the API Response Guard

You have a function that processes an API response. The response can either be a User object or an ErrorResponse object. Currently, the code uses a type assertion to access the user's name, which is risky. Refactor the processResponse function to use equality narrowing instead of the as keyword to safely access the username.

type User = {
  type: "success";
  username: string;
};

type ErrorResponse = {
  type: "error";
  message: string;
};

type ApiResponse = User | ErrorResponse;

function processResponse(response: ApiResponse) {
  if (response.type === "error") {
    console.error("Error: " + response.message);
    return;
  }

  // FIX THIS: Remove the type assertion and rely on narrowing
  const user = response as User; 
  console.log("Welcome, " + user.username);
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.