Equality narrowing
Narrows types by comparing values with ===, !==, == and !=, including the null-and-undefined shortcut.
x === y | x !== y | x == null | x != null- Added in TypeScript 2.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$x | T | Left operand. Narrows in both branches when compared against a literal, another narrowable value, or null/undefined. |
$y | U | Right operand. When it is a literal type the comparison acts as a discriminant check; when it is another variable both sides narrow to the common type. |
Return value
booleanA boolean. On a true ===, both operands narrow to the intersection of their types; on a true != null, the operand loses both null and undefined.
Errors
Nothing. tsc reports "This comparison appears to be unintentional" when the two operand types have no overlap.
Changelog
- PHP 2.0Non-null and non-undefined type guards introduced under --strictNullChecks: == null and != null remove both null and undefined, while === null and === undefined remove only one.
- PHP 4.4An equality check assigned to a const narrows the original value when the const is later tested.
- PHP 5.5A single-return arrow such as `x => x !== undefined` now infers the type predicate `x is NonNullable<T>`, so `.filter(x => x !== undefined)` narrows the array element type.
Equality narrows in three distinct ways, and it is worth keeping them apart.
Against a literal
shape.kind === "circle" selects the matching union member and removes it in the else. This is the mechanism behind discriminated unions; a switch is just a stack of these.
Against another variable
If x: string | number and y: string | boolean are ===, both must be the only type they share, so both narrow to string inside the branch. The compiler will also refuse comparisons with no overlap at all, reporting "This comparison appears to be unintentional because the types have no overlap."
Against null and undefined
This is the one worth memorising. Loose == null is true for both null and undefined, and TypeScript models that exactly: if (value != null) strips both from the type in one check. Strict !== null removes only null and leaves undefined behind, which is a common cause of "possibly undefined" errors that look like they should already be handled.
What trips people up
Unlike a truthiness test, equality narrowing keeps 0 and "", which is exactly why it is the right guard for optional numbers and strings.
Example
When to use it
- Checking an optional numeric field where 0 is a legitimate value
- Stripping null and undefined from a mapped array in one comparison
- Selecting a union member by its literal tag inside an if rather than a switch
Frequently asked questions
What is the difference between == null and === null in TypeScript?
`== null` is true for both null and undefined and narrows both away in one check; `=== null` removes only null and leaves undefined in the type. That is why code using `!== null` often still reports "possibly undefined".
Why does TypeScript say my comparison appears to be unintentional?
The two operand types have no values in common, so the comparison can never be true — for example comparing a string literal union against a value that cannot hold it. Fix the types rather than casting.
Should I use == null even though my linter bans loose equality?
Yes, and most configurations already allow it: ESLint's eqeqeq rule has a `null: "ignore"` option precisely because `== null` is the idiomatic single check for both nullish values.
More examples
== null versus === null, and why 0 survives
Prints 0.0C n/a n/a then 0.0C n/a. Two things to notice: `!= null` handles both absent cases in one line while `!== null` needs a second check, and 0 comes through as "0.0C" in both — a truthiness guard would have reported it as n/a.
type Reading = { celsius?: number | null };
function loose(r: Reading): string {
if (r.celsius != null) return `${r.celsius.toFixed(1)}C`; // number
return "n/a";
}
function strictOnly(r: Reading): string {
if (r.celsius !== null) {
// r.celsius: number | undefined — still needs another check,
// so r.celsius.toFixed(1) would be a compile error here.
return r.celsius === undefined ? "n/a" : `${r.celsius.toFixed(1)}C`;
}
return "n/a";
}
console.log(loose({ celsius: 0 }), loose({ celsius: null }), loose({}));
console.log(strictOnly({ celsius: 0 }), strictOnly({}));Inferred predicates make filter work (5.5+)
Prints [ 'GREY JAY', 'BALD EAGLE' ]. Before TypeScript 5.5, `b.toUpperCase()` errored with "'b' is possibly 'undefined'" and you had to write `.filter((b): b is string => b !== undefined)`. The truthiness variant is still rejected for inference because returning false would not prove the value is not a string — an empty string is falsy.
const nationalBirds = new Map<string, string>([
["ca", "Grey Jay"],
["us", "Bald Eagle"],
]);
const countries = ["ca", "fr", "us"];
// x !== undefined is a single-return refinement of the parameter,
// so TS 5.5 infers `x is string` and birds is string[], not (string|undefined)[].
const birds = countries
.map((c) => nationalBirds.get(c))
.filter((b) => b !== undefined);
console.log(birds.map((b) => b.toUpperCase()));
// A truthiness callback does NOT infer a predicate:
// .filter(b => !!b) still yields (string | undefined)[].
Discussion