Let’s be honest, you’ve been using === and !== since you learned JavaScript. You know they’re the strict equality operators, the ones that actually check both value and type, saving you from the bizarre coercion behavior of their == and != cousins. But here’s the beautiful part: in TypeScript, these workhorse operators aren’t just for comparisons; they’re a fundamental tool for narrowing types. The type checker watches your if and else statements like a hawk, learning from your logic and refining types accordingly.

This is called equality narrowing, and it’s one of the simplest yet most powerful forms of type narrowing. The concept is gloriously straightforward: when you compare a variable of a union type with a literal value using ===, !==, ==, or !=, TypeScript can remove entire branches from that union.

The Basic, Beautiful Mechanism

Imagine you have a variable that can be one of several literal types. The most common use case is handling different string literals, perhaps for a function that dispatches events.

function printState(state: "success" | "error" | "pending") {
  // Right now, `state` is either "success", "error", or "pending"

  if (state === "success") {
    // In here, TypeScript knows `state` can ONLY be "success"
    console.log("All is well!");
  } else if (state === "error") {
    // And in here, it can ONLY be "error"
    console.error("Everything is on fire!");
  } else {
    // And here, by beautiful, logical deduction...
    // `state` must be the one remaining option: "pending"
    console.log("Please hold...");
  }
}

TypeScript isn’t just guessing. It follows the path of your code. The first if condition is only true if state is exactly "success", so it narrows the type to that literal within that block. The else if does the same for "error". After both checks fail, the else block is the only logical place left, so the type is narrowed to the last remaining member of the union: "pending". It feels almost stupidly simple, but this is the bedrock of writing type-safe conditional logic.

It’s Not Just for Strings

While string literals are the poster child, this works with all primitive types: numbers, booleans, and even null and undefined. The latter is incredibly useful for dealing with values that might be absent.

function getLength(s: string | null) {
  // Classic pitfall: if we try s.length here, TS will yell
  // because `s` might be null.

  if (s === null) {
    // In this branch, type is narrowed to `null`
    return 0;
  } else {
    // And here, because we've eliminated `null`,
    // the type is narrowed to `string`. Safe!
    return s.length;
  }
}

You can flip the logic with !== just as effectively. This is often more elegant.

function getLengthBetter(s: string | null) {
  if (s !== null) {
    // Type is narrowed to `string` here.
    return s.length;
  }
  return 0;
}

The == null Convention for a Double Check

Here’s a pro-trip that saves a few keystrokes and is widely used in the ecosystem. Because null == undefined is true (a rare sensible choice in the abstract equality world), you can use == null or != null to check for both null and undefined simultaneously.

function example(value: string | null | undefined) {
  if (value != null) {
    // Type is narrowed to `string`.
    // We've eliminated both `null` AND `undefined`.
    console.log(value.toUpperCase());
  }

  // This also works for checking if it *is* nullish.
  if (value == null) {
    // Type is narrowed to `null | undefined`
    console.log('Value is null or undefined');
  }
}

This is a convention you’ll see everywhere. It’s concise and effective. Just remember it relies on the slightly-frowned-upon ==, but in this one specific case, it’s not just acceptable—it’s idiomatic.

A Common Pitfall: Indirect Comparisons

The type narrowor is smart, but it’s not psychic. It only narrows based on the condition you write directly. A common mistake is to store the result of a comparison and use it later.

function doSomething(x: string | number) {
  const isString = (typeof x === 'string');

  if (isString) {
    console.log(x.toUpperCase()); // Error! 🚨
    // TypeScript thinks: "What is `x`? Oh, it's still `string | number`.
    // What is `isString`? It's a `boolean`. I have no idea if they're related."
  }
}

The type guard—the act of narrowing—only happens if the check is done inline within the conditional. This is because control flow analysis can’t track the relationship between a variable (isString) and another variable (x) after the fact. Always put your === check directly in the if statement.

Equality narrowing is your first and most straightforward line of defense against the ambiguity of union types. It turns your everyday comparison operators into powerful tools for making your code more precise and, consequently, more robust. It’s the type system working with your existing JavaScript intuition, not against it.