== vs === : Loose vs Strict Equality
Short answer
=== returns true only when both operands have the same type and the same value. == converts the operands to a common type first and then compares, which makes results like 0 == "" true. Use === everywhere, with one deliberate exception: x == null, which catches both null and undefined.
| Expression | == (loose) | === (strict) |
|---|---|---|
| 1 == "1" | true | false |
| 0 == "" | true | false |
| 0 == false | true | false |
| null == undefined | true | false |
| null == 0 | false | false |
| NaN == NaN | false | false |
| [] == "" | true | false |
| [1] == 1 | true | false |
| {} == {} | false | false |
What each operator does
Strict equality has one rule: if the two operands have different types, the answer is false; otherwise compare the values. That is the whole specification, minus two special cases for NaN and signed zero. It is predictable because it never converts anything.
Loose equality does the same thing when the types already match — for identical types, == and === behave identically. The difference only appears when the types differ, at which point == applies a conversion procedure and compares the results. Every surprising output you have seen comes from that procedure, and it is short enough to actually learn.
11 == 1; // → true
21 === 1; // → true
3"a" == "a"; // → true
4"a" === "a"; // → true
5
6// The operators only diverge across types:
71 == "1"; // → true "1" is converted to 1
81 === "1"; // → false number is not stringThe coercion rules, in the order they apply
The abstract equality comparison is a short list of ordered rules. Once you know them, no == result is surprising — they are all mechanical consequences.
- If the types are the same, compare as === does.
- null == undefined is true. Neither is loosely equal to anything else.
- Number compared with string: convert the string to a number.
- Boolean compared with anything: convert the boolean to a number first — false becomes 0, true becomes 1.
- Object compared with a primitive: convert the object to a primitive with valueOf, then toString.
- Anything else is false.
1// Rule 2 — the null/undefined pair, and nothing else
2null == undefined; // → true
3null == 0; // → false null is not converted to a number
4undefined == ""; // → false
5
6// Rule 3 — string becomes a number
7"5" == 5; // → true Number("5") is 5
8"" == 0; // → true Number("") is 0
9" \t\n " == 0; // → true whitespace-only strings are 0
10
11// Rule 4 — booleans always become numbers first
12false == 0; // → true
13false == ""; // → true false → 0, "" → 0
14true == "1"; // → true true → 1, "1" → 1
15true == "true"; // → false Number("true") is NaNRule 4 is where most people are caught. A boolean is never compared as a truthiness test — it is converted to a number, and then the other operand is converted to a number too. That is why true == "true" is false while true == "1" is true, which reads like nonsense until you see the two conversions.
The famous surprises, explained
The quiz-question results are all the same three rules applied twice. Being able to derive them, rather than remembering them, is what makes the answer convincing.
10 == "";
2// string vs number → Number("") is 0 → 0 == 0
3// → true
4
5"" == "0";
6// same type, no conversion, "" is not "0"
7// → false (so == is not transitive)
8
9[] == 0;
10// object vs number → [].toString() is "" → Number("") is 0
11// → true
12
13[] == ![];
14// ![] is false (every object is truthy) → [] == false
15// → boolean becomes 0 → [] becomes "" becomes 0 → 0 == 0
16// → true
17
18[1,2] == "1,2";
19// object vs string → Array.prototype.toString joins with commas
20// → true
21
22null >= 0;
23// → true relational operators convert null to 0…
24null == 0;
25// → false …but == has a special rule that does not
26This is 1 of 80+ questions in the JavaScript Interview Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the JavaScript Interview Kit → ₹299NaN, signed zero, and Object.is
Strict equality has two behaviours that are not about types at all. NaN is not equal to itself, which is required by the IEEE 754 floating-point standard and is why you cannot test for NaN by comparison. And +0 and -0 compare as equal even though they are distinguishable values.
1NaN === NaN; // → false
2[NaN].includes(NaN); // → true includes uses SameValueZero
3[NaN].indexOf(NaN); // → -1 indexOf uses ===
4
5Number.isNaN(NaN); // → true the correct test
6isNaN("hello"); // → true the OLD global coerces first — avoid
7
8+0 === -0; // → true
9Object.is(+0, -0); // → false
10Object.is(NaN, NaN); // → true
11
121 / +0; // → Infinity
131 / -0; // → -Infinity the difference is observableObject.is implements what the specification calls SameValue: identical to === except that it treats NaN as equal to itself and distinguishes the two zeros. It is not a deep comparison, and it is not a general replacement for === — reach for it when you specifically need those two behaviours. React uses it internally for dependency and prop comparisons, which is worth knowing when a memo unexpectedly does or does not bail out.
The one place == is the right choice
x == null is true for exactly two values, null and undefined, and false for everything else — including 0, empty string and false. It is the shortest way to express "this has no value", it is explicitly allowed by the default ESLint eqeqeq rule with the smart option, and it appears throughout well-written library code.
1if (value == null) return "unknown";
2// → catches null and undefined only.
3// Equivalent to: value === null || value === undefined
4
5// It composes with the nullish operators, which use the same test:
6const name = user.name ?? "Anonymous";
7// → falls back only for null/undefined, not for "" or 0
8const city = user.address?.city;
9// → undefined if address is null or undefined, no throw
10
11// Compare with the truthiness check, which is a different question:
12if (!value) return "unknown";
13// → also catches 0, "", NaN and false — usually not what you meantComparing objects: neither operator does what you want
Both operators compare objects by reference. Two objects with identical contents are never equal, because they are two different objects in memory. This trips people up because it looks like an equality problem when it is really the reference model at work.
1{ a: 1 } === { a: 1 }; // → false
2[1,2] === [1,2]; // → false
3
4const x = { a: 1 };
5const y = x;
6x === y; // → true same reference
7
8// Comparing contents needs an explicit strategy:
9JSON.stringify(a) === JSON.stringify(b);
10// → works for plain, JSON-safe data, but key ORDER matters
11// and it breaks on Dates, Maps, undefined and cycles
12
13// A shallow comparison, which is what React.memo does:
14const shallowEqual = (a, b) =>
15 Object.keys(a).length === Object.keys(b).length &&
16 Object.keys(a).every(k => Object.is(a[k], b[k]));
17// → true for { a: 1 } and { a: 1 }, false if any value differsThis is the same rule that makes React skip a render when you mutate state instead of replacing it, and the same rule that makes an inline object prop defeat memoisation. Equality of objects in JavaScript is identity, not contents, everywhere — the comparison operators are just where you first notice it.
Where else strict equality is used for you
Several language constructs compare with === or something close to it, which matters if you are in the habit of relying on coercion. A switch statement matches cases strictly, so a numeric string will not match a numeric case. Array methods split between two rules: indexOf uses ===, while includes uses SameValueZero, which is why only one of them finds NaN.
1const status = "1";
2switch (status) {
3 case 1: console.log("number"); break;
4 case "1": console.log("string"); break;
5}
6// → "string" switch never coerces
7
8[1, 2, 3].indexOf("2"); // → -1 === under the hood
9[1, 2, 3].includes("2"); // → false SameValueZero, still strict
10
11new Set([1, "1"]).size; // → 2 Sets use SameValueZero
12new Map([[1, "a"]]).get("1"); // → undefined keys are strict tooThe practical consequence is that a codebase which relies on loose comparison behaves inconsistently: a value that matches in an if statement fails to match the equivalent switch case, and an id that a lookup finds by == is missed by a Map keyed on the same value. Those bugs are hard to spot in review because both lines look correct in isolation. Normalising the type once — at the point where the value enters your code, usually a parsed query string or a form field — removes the whole category, and it is a better habit than remembering which construct coerces.
That is the argument for strict equality in one sentence: it is not that == gives wrong answers, but that it gives answers you have to derive rather than read. Code is read far more often than it is written, and a comparison whose result depends on three conversion rules is a comment waiting to be needed.
Is === faster than ==?
Marginally, because it can skip the conversion step, but the difference is far too small to matter in application code. Prefer it for predictability. If a comparison is genuinely on a hot path, the type check is not what is costing you.
What does [] == ![] return, and why?
true. ![] is false because every object is truthy, so this becomes [] == false. The boolean converts to 0, the array converts to "" and then to 0, and 0 == 0 is true. It is a chain of three rules, which is why it makes a good exam of whether you know them.
Why is NaN not equal to itself?
Because IEEE 754 defines it that way — NaN represents an indeterminate result, and two indeterminate results are not known to be the same. Use Number.isNaN to test for it, or Object.is, which treats NaN as equal to itself.
Does TypeScript stop me using ==?
No. TypeScript will flag a comparison between types that can never overlap, but a == that merely coerces compiles without complaint. Loose equality is a lint concern, not a type-checking one, so enable the eqeqeq rule if you want it enforced.
What is the difference between == null and !value?
They answer different questions. value == null is true only for null and undefined. !value is a truthiness test, so it is also true for 0, "", NaN and false. Using the truthiness check where you meant the null check is a common source of bugs with numeric and text inputs.
Frequently asked questions
- What is the difference between == and === in JavaScript?
- === is strict equality: if the operands have different types the result is false, with no conversion. == is loose equality: when the types differ it converts them to a common type and then compares, which is why 1 == "1" is true and 1 === "1" is false.
- Should I always use === in JavaScript?
- Almost always. The one accepted exception is x == null, which tests for null and undefined together and is explicitly permitted by the standard ESLint rule. Everywhere else, strict equality removes a class of bugs at no cost.
- Why does 0 == "" return true?
- When a number is compared with a string, the string is converted to a number. Number("") is 0, so the comparison becomes 0 == 0. The same rule makes " " == 0 true, because a whitespace-only string also converts to 0.
- Why does [] == ![] return true?
- ![] is false, since all objects are truthy. That gives [] == false. Booleans convert to numbers, so false becomes 0. The array converts to a primitive — an empty string — which then converts to 0. Both sides are 0, so the result is true.
- Is null equal to undefined?
- With ==, yes — the specification has an explicit rule making them loosely equal to each other and to nothing else. With ===, no, because they are different types. That special rule is exactly what makes the x == null idiom useful.
- Why is NaN not equal to NaN?
- IEEE 754 defines NaN as unequal to every value including itself, because it stands for an indeterminate result. Test for it with Number.isNaN, or use Object.is(x, NaN), which treats NaN as equal to itself.
- What is the difference between === and Object.is?
- Object.is matches === except in two cases: it returns true for Object.is(NaN, NaN) and false for Object.is(+0, -0). It is not a deep comparison. React uses this SameValue semantics internally when comparing props and hook dependencies.
- Does === compare objects by value?
- No. Both operators compare objects by reference, so two objects with identical contents are never equal. Comparing contents requires an explicit strategy — a shallow comparison of keys, or a deep-equality helper for nested data.
- Is === faster than ==?
- Slightly, because there is no conversion step, but the difference is negligible in real code. Choose === for predictability and readability rather than for speed.
- Does the switch statement use == or ===?
- ===. Cases are matched strictly, so case 1 will not match the string "1". This surprises people who are used to loose comparison elsewhere, and it is a common source of a switch that silently falls through to the default.
- Why does indexOf not find NaN but includes does?
- indexOf compares with ===, and NaN === NaN is false. includes uses SameValueZero, which treats NaN as equal to itself. That is the only difference in comparison behaviour between the two methods.
- What does the eqeqeq ESLint rule do?
- It reports every use of == and !=. With the "smart" option it allows comparison against null, along with a couple of other narrow cases, which encodes the usual convention: strict equality everywhere except the deliberate null check.
This is 1 of 80+ questions in the JavaScript Interview Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the JavaScript Interview Kit → ₹299