JavaScript == vs ===: Loose vs Strict Equality Explained
JavaScript's == converts types before comparing; === does not. How coercion works, the classic gotchas, where Object.is fits, and which to use.
JavaScript has two equality operators. Strict equality (===) returns true only when both values have the same type and the same value. Loose equality (==) first tries to convert the operands to a common type — a process called type coercion — and then compares them. That one extra step is responsible for most of the language’s famous equality surprises, and it’s why almost every style guide tells you to default to ===.
What strict equality does
=== is the simple one. If the two operands are different types, the answer is false, full stop. If they’re the same type, it compares values:
1 === 1 // true
1 === "1" // false — number vs string
"abc" === "abc" // true
true === 1 // false
null === undefined // false
For objects, arrays, and functions, “same value” means same reference. Two arrays with identical contents are still two different objects:
[1, 2] === [1, 2] // false
const a = [1, 2];
a === a // true
That reference rule applies to both operators. Neither == nor === does a structural comparison; if you need one, compare fields yourself or reach for a deep-equality utility. The same distinction drives the difference between shallow and deep copies.
What loose equality does
== follows a longer algorithm from the language specification. When the types differ, it converts one or both operands and tries again. The main rules:
- Same type? Behave exactly like
===. nullandundefinedare equal to each other and to nothing else.- Number vs string — convert the string to a number.
- Boolean vs anything — convert the boolean to a number first (
truebecomes1,falsebecomes0), then compare again. - Object vs primitive — convert the object to a primitive (via
valueOfortoString), then compare again. - BigInt vs number or string — compare the mathematical values.
Each conversion can trigger another rule, so a single comparison may hop through several steps before it settles on an answer.
The classic gotchas
Once you know the rules, the notorious examples stop looking random:
"" == 0 // true — "" converts to 0
"0" == 0 // true — "0" converts to 0
"" == "0" // false — both strings, compared as strings
false == "0" // true — false → 0, "0" → 0
[] == false // true — [] → "" → 0, false → 0
[] == ![] // true — ![] is false, then as above
null == 0 // false — null only equals undefined
"\n" == 0 // true — whitespace-only strings convert to 0
Notice that loose equality isn’t even transitive: "" == 0 and 0 == "0" are both true, yet "" == "0" is false. Code that relies on == behaving like mathematical equality will eventually hit one of these edges, usually with user input that arrives as a string.
A related trap involves relational operators. null == 0 is false, but null >= 0 is true, because >= converts null to a number while == has its own special case for it. Equality and ordering in JavaScript don’t follow one shared set of conversion rules.
The NaN and signed-zero edge cases
Two values break even ===:
NaNis not equal to anything, including itself.NaN === NaNisfalse. UseNumber.isNaN(x)to test for it.+0and-0compare as equal:0 === -0istrue, even though they’re distinct values (1 / -0is-Infinity).
Object.is exists for exactly these cases. It behaves like === except that Object.is(NaN, NaN) is true and Object.is(0, -0) is false. The spec calls this “SameValue” comparison.
There’s a fourth flavor you use constantly without naming it: SameValueZero. It treats NaN as equal to itself but treats +0 and -0 as equal. Array.prototype.includes, Map keys, and Set membership all use it, which is why these two lines disagree:
[NaN].indexOf(NaN) // -1 — indexOf uses ===
[NaN].includes(NaN) // true — includes uses SameValueZero
If you store keys in a Map or Set, SameValueZero is the rule deciding whether two keys collide.
Comparing all four equality algorithms
== (loose) | === (strict) | Object.is (SameValue) | SameValueZero | |
|---|---|---|---|---|
| Converts types | Yes | No | No | No |
1 vs "1" | true | false | false | false |
null vs undefined | true | false | false | false |
NaN vs NaN | false | false | true | true |
+0 vs -0 | true | true | false | true |
| Objects | By reference | By reference | By reference | By reference |
| Where you see it | Legacy code, x == null | Default choice | React state checks, edge-case tests | includes, Map, Set |
The one idiomatic use of ==
There’s a single pattern many teams deliberately allow: x == null. Because null and undefined are loosely equal only to each other, x == null is a compact way to write x === null || x === undefined. It won’t match 0, "", or false, so it’s safer than a plain truthiness check.
Modern syntax covers most of the same ground more explicitly. The optional chaining and nullish coalescing operators (?. and ??) are built on that same “null or undefined” test, so you often don’t need the comparison at all:
const port = config.port ?? 3000;
If you use ESLint, the eqeqeq rule enforces === everywhere, and its "smart" option carves out the == null exception. Pairing it with a formatter is covered in ESLint vs Prettier.
Does TypeScript make this moot?
Partly. TypeScript flags many comparisons between obviously unrelated types — comparing a number to a string literal, for instance, produces an error that the types have no overlap. That catches a lot of accidental coercion at compile time.
But types are erased at runtime. Data from JSON.parse, form fields, query strings, and third-party APIs can still be the “wrong” type no matter what your annotations claim, especially if those values were typed as any. Using === keeps runtime behavior predictable even when the type information turns out to be a lie.
Performance is not the reason
You’ll sometimes read that === is faster because it skips coercion. In practice, modern engines optimize both heavily, and when operand types already match, the two do the same work. Choose === because it’s easier to reason about, not to save nanoseconds.
The takeaway
=== compares type and value with no conversions; == coerces operands through a multi-step algorithm that produces non-transitive, surprising results. Default to ===, allow x == null if your team finds it clearer than ??, use Number.isNaN or Object.is for the NaN and signed-zero edge cases, and remember that every equality operator compares objects by reference, not by contents.
Tagged
Keep reading
Takina · · 5 min read JavaScript Strings and Unicode: Code Units vs Graphemes
JavaScript string length counts UTF-16 code units, not characters. How code units, code points, and grapheme clusters differ and how to handle each.
Takina · · 5 min read Event Bubbling vs Capturing in JavaScript, Explained
Event bubbling and capturing are the two directions a DOM event travels. How propagation phases work, when to use each, and how to stop them.
Takina · · 4 min read Object.groupBy() in JavaScript, Explained
Object.groupBy() buckets array items by a key you compute, replacing the reduce() boilerplate developers have written for years. How it works.