skip to content

In TypeScript with strictNullChecks enabled, how do `if (x === null)`, `if (x === undefined)` and `if (x == null)` differ in what they narrow out of a value declared `string | null | undefined`?

level: middleimportance: must knowfreq 60%

answer

  1. three constituents, three different splits
  2. strict comparison removes exactly one
  3. loose nullish check is special-cased
  4. == null is not a truthiness test
  5. needs strictNullChecks to mean anything

basics

~20 s

Strict comparisons remove one constituent each: after x === null returns false the type is string | undefined, and after x === undefined it is string | null. The loose form x == null is special-cased to cover both, so its false branch is plain string.

solid answer

~40 s

All three are recognised narrowing conditions, they just cover different constituents. `x === null` splits the union into `null` and `string | undefined`; `x === undefined` splits it into `undefined` and `string | null`. The checker deliberately special-cases loose equality against `null` or `undefined`: `x == null` narrows to `null | undefined` in the true branch and to `string` in the false branch, which is why `if (x != null)` is the idiomatic one-comparison presence guard. `if (x !== null && x !== undefined)` reaches the same `string` and reads more explicitly if your style guide bans `==`. Note this only matters with `strictNullChecks` on — otherwise `null` and `undefined` are not part of the union to begin with, so there is nothing to remove.

code

typescript · 14 lines
typescript
function demo(x: string | null | undefined) {
  if (x === null) {
    const a: null = x;
    void a;
  } else {
    const b: string | undefined = x; // undefined survives
    void b;
  }

  if (x != null) {
    const c: string = x; // both nullish members removed
    void c;
  }
}

go deeper

for a junior

Remember that null and undefined are two different values, and that x != null is the short way to rule out both before touching a property.

for a middle

State the three resulting types precisely, and explain that the checker special-cases loose comparison against null/undefined so a single test removes both constituents.

for a senior

Show judgment about when the distinction carries meaning — an API that uses null for "cleared" and undefined for "absent" — and defend the guard style you would enforce across a codebase.

for a principal

Own the boundary policy: whether both nullish values may reach domain code at all, how adapters normalise to one of them, and what that buys in reviewability across teams.

## Two nullish values, three ways to ask Under `strictNullChecks`, `null` and `undefined` are ordinary union constituents rather than values assignable to everything, so a variable typed `string | null | undefined` really does have three constituents that a condition can remove. TypeScript recognises equality against the literals `null` and `undefined` as a narrowing condition and applies it in both branches. ```ts function demo(x: string | null | undefined) { if (x === null) { x; // null } else { x; // string | undefined } if (x === undefined) { x; // undefined } else { x; // string | null } if (x == null) { x; // null | undefined } else { x; // string } } ``` ## The special case worth naming The third form is not "loose equality happening to work". The compiler models it deliberately: a comparison of the form `x == null` or `x != null` (and equally `== undefined`) is treated as a test for *either* nullish value, so one comparison removes both constituents. That is the type-layer reason the `!= null` idiom survives in codebases that otherwise forbid `==`. Why the runtime operator behaves that way is a JavaScript question about the equality algorithm; what matters here is that the checker knows about it and gives you the precise narrowing. The symmetric form is the one you actually write in guard position: ```ts function len(x: string | null | undefined): number { if (x != null) { return x.length; // x: string } return 0; } ``` ## Choosing between them - **You care which one it is.** APIs sometimes give `null` and `undefined` different meanings — `null` meaning "explicitly cleared", `undefined` meaning "not supplied". Then `x === null` is the right question, and the resulting `string | undefined` in the else branch is information you want. - **You only care about presence.** Use `x != null`, or the explicit `x !== null && x !== undefined` if `==` is banned. Both land on `string`. - **You must keep falsy values.** All of these are better than `if (x)` whenever `""` or `0` are legitimate data, because they narrow away exactly the nullish constituents and nothing else. ## Optional properties and parameters A property written `y?: string` has type `string | undefined`, so `null` never appears in it. Both `x !== undefined` and `x != null` narrow it to `string`; the loose form simply also covers the case where the declared type later grows a `null`. Writing `x === null` against such a value is asking about a constituent that is not there, and the compiler pushes back on comparisons between types with no overlap. ## The negated and combined forms Narrowing follows the boolean structure of the condition, so all of these behave as you would expect: ```ts declare const a: string | null | undefined; if (!(a == null)) { a; // string } function f(a: string | null, b: number | undefined) { if (a != null && b != null) { a.length + b; // a: string, b: number } } ``` The `&&` chain narrows each operand for the code to its right, which is what makes the guard readable. ## Without strictNullChecks With the flag off, `null` and `undefined` are assignable to every type and are not tracked as union members, so `string | null | undefined` collapses to `string` and none of these comparisons change anything statically. The runtime check still runs — the type layer is erased either way — but the compiler stops helping you, which is the practical argument for turning the flag on before arguing about which comparison to prefer. ## Interview framing The answer an interviewer is listening for is: name the three splits precisely, then say that `== null` is a deliberate special case in the checker covering both nullish values in one comparison. Candidates who claim `x == null` "also matches 0 or the empty string" are confusing it with a truthiness check — it does not, and that is exactly the property that makes it useful.

  • If a parameter is declared `x?: string`, what does `x == null` narrow it to in each branch?
    An optional parameter has type `string | undefined`, so the true branch is `undefined` and the false branch is `string`. The loose form costs nothing here and keeps working if the declared type later gains a `null`. `x !== undefined` gives the same result and reads more explicitly.
  • What changes about these checks when strictNullChecks is turned off?
    Nothing statically. Without the flag, `null` and `undefined` are assignable to every type and never appear as union constituents, so `string | null | undefined` is effectively `string` and there is nothing for the comparison to remove. The runtime test still runs; you have simply lost the compiler's help.

saying these in an interview costs you the question

  • Claims x == null also matches 0 or the empty string
  • Thinks x === null also excludes undefined
  • Says == and === narrow identically for nullish values
  • Believes narrowing still helps with strictNullChecks off
  • Uses if (x) whenever the intent is a presence check

context