Learn JavaScript equality: == vs ===, typeof quirks, and Object.is(). Understand type coercion and why NaN !== NaN.
Why does 1 == "1" return true but 1 === "1" return false? Why does typeof null return "object"? And why is NaN the only value in JavaScript that isn’t equal to itself?
Copy
Ask AI
// Same values, different resultsconsole.log(1 == "1"); // true — loose equality converts typesconsole.log(1 === "1"); // false — strict equality checks type first// The famous quirksconsole.log(typeof null); // "object" — a bug from 1995!console.log(NaN === NaN); // false — NaN never equals anything
Understanding JavaScript’s equality operators is crucial because comparison bugs are among the most common in JavaScript code. According to the ECMAScript specification (ECMA-262), JavaScript defines three distinct equality algorithms: Abstract Equality, Strict Equality, and SameValue. This guide will teach you exactly how ==, ===, and Object.is() work, and when to use each one.
What you’ll learn in this guide:
The difference between == (loose) and === (strict) equality
How JavaScript converts values during loose equality comparisons
The typeof operator and its famous quirks (including the null bug)
When to use Object.is() for edge cases like NaN and -0
Common comparison mistakes and how to avoid them
A simple rule: when to use which operator
Prerequisites: This guide assumes you understand Primitive Types and Type Coercion. Equality operators rely heavily on how JavaScript converts types. If those concepts are new to you, read those guides first!
JavaScript provides three ways to compare values for equality. Here’s the quick summary:
Operator
Name
Type Coercion
Best For
==
Loose (Abstract) Equality
Yes
Checking null/undefined only
===
Strict Equality
No
Default choice for everything
Object.is()
Same-Value Equality
No
Edge cases (NaN, ±0)
Copy
Ask AI
// The same comparison, three different resultsconst num = 1;const str = "1";console.log(num == str); // true (coerces string to number)console.log(num === str); // false (different types)console.log(Object.is(num, str)); // false (different types)
The simple rule: Always use === for comparisons. The only exception: use == null to check if a value is empty (null or undefined). You’ll rarely need Object.is(). It’s for special cases we’ll cover later.
Imagine a teacher grading a math test. The question asks: “What is 2 + 2?”One student writes: 4
Another student writes: "4" (as text)
A third student writes: 4.0How strict should the teacher be when grading?
Loose equality (==) — The relaxed teacher. Accepts 4 and "4" as the same answer because the meaning is similar. Converts values to match before comparing.
Strict equality (===) — The strict teacher. Only accepts the exact answer in the exact format. The number 4 and the string "4" are different answers.
typeof — Asks “What kind of answer is this?” Is it a number? A string? Something else?
Object.is() — The most precise teacher. Even stricter than === — can spot tiny differences that others miss.
TL;DR: Use === for almost everything. Use == null to check for both null and undefined. Use Object.is() only for NaN or -0 edge cases.
The == operator tries to be helpful. Before comparing two values, it converts them to the same type. This automatic conversion is called type coercion.For example, if you compare the number 5 with the string "5", JavaScript thinks: “These look similar. Let me convert them and check.” So 5 == "5" returns true.
This example shows why == can produce unexpected results. An empty array appears to equal its own negation! This isn’t a bug. It’s how JavaScript’s conversion rules work. This is why most developers prefer ===.
Despite its quirks, there’s one legitimate use case for loose equality:
Copy
Ask AI
// Checking for null OR undefined in one comparisonfunction greet(name) { // Using == (the one acceptable use case!) if (name == null) { return "Hello, stranger!"; } return `Hello, ${name}!`;}// Both null and undefined are caughtgreet(null); // "Hello, stranger!"greet(undefined); // "Hello, stranger!"greet("Alice"); // "Hello, Alice!"greet(""); // "Hello, !" (empty string is NOT null)greet(0); // "Hello, 0!" (0 is NOT null)
This is equivalent to the more verbose:
Copy
Ask AI
function greet(name) { if (name === null || name === undefined) { return "Hello, stranger!"; } return `Hello, ${name}!`;}
Many style guides (including those from Airbnb and StandardJS) make an exception for value == null because it’s a clean way to check for “nullish” values. However, you can also use the nullish coalescing operator (??) or optional chaining (?.) introduced in ES2020.
The strict equality operator compares two values without any conversion. If the types are different, it immediately returns false.This is the operator you should use almost always. It’s simple and predictable: the number 1 and the string "1" are different types, so 1 === "1" returns false. No surprises.
Even === has two edge cases that might surprise you:
NaN !== NaN
+0 === -0
Copy
Ask AI
// NaN is the only value that is not equal to itselfNaN === NaN // false!// NaN doesn't equal anything, not even itself!// This is part of how numbers work in all programming languages// This is by design (IEEE 754 specification)// NaN represents "Not a Number" - an undefined result// Since it's not a specific number, it can't equal anything// How to check for NaN:Number.isNaN(NaN) // true (recommended)isNaN(NaN) // true (but has quirks — see below)Object.is(NaN, NaN) // true (ES6)// The isNaN() quirk:isNaN("hello") // true! (converts to NaN first)Number.isNaN("hello") // false (no conversion)
Always use Number.isNaN() instead of the global isNaN(). The global isNaN() function converts its argument to a Number first, which means isNaN("hello") returns true. That’s rarely what you want.
Copy
Ask AI
// Positive zero and negative zero are considered equal+0 === -0 // true-0 === 0 // true// But they ARE different! Watch this:1 / +0 // Infinity1 / -0 // -Infinity// Two zeros, two different infinities. Math is wild.// How to distinguish them:Object.is(+0, -0) // false (ES6)1 / +0 === 1 / -0 // false (Infinity vs -Infinity)// When does -0 appear?0 * -1 // -0Math.sign(-0) // -0JSON.parse("-0") // -0
You’ll rarely need to tell +0 and -0 apart unless you’re doing advanced math or physics calculations.
ES6 introduced Object.is() to fix the two edge cases where === gives unexpected results. As documented by MDN, Object.is() implements the “SameValue” algorithm from the ECMAScript specification. It works exactly like ===, but handles NaN and -0 correctly.
// The two cases where === is "wrong"NaN === NaN // false (but NaN IS NaN!)+0 === -0 // true (but they ARE different!)// Object.is() fixes bothObject.is(NaN, NaN) // true ✓Object.is(+0, -0) // false ✓
// 1. Checking for NaN (alternative to Number.isNaN)function isReallyNaN(value) { return Object.is(value, NaN);}// 2. Distinguishing +0 from -0 (rare, but needed in math/physics)function isNegativeZero(value) { return Object.is(value, -0);}// 3. Implementing SameValue comparison (like in Map/Set)// Maps use SameValueZero (like Object.is but +0 === -0)const map = new Map();map.set(NaN, "value");map.get(NaN); // "value" (NaN works as a key!)// 4. Library code and polyfills// When you need exact specification compliance
For most everyday code, you won’t need Object.is(). Use === as your default, and reach for Object.is() only when you specifically need to handle NaN or ±0 edge cases.
The typeof operator tells you what type a value is. It returns a string like "number", "string", or "boolean". The 2023 State of JS survey found that TypeScript adoption continues to grow — partly driven by developers seeking to avoid the type-checking pitfalls that typeof alone cannot solve. It’s very useful, but it has some famous quirks that surprise many developers.
Why? This is a bug from JavaScript’s first version in 1995. In the original code, values were stored with a type tag. Objects had the tag 000, and null was represented as 0x00, which also matched the object tag.Why wasn’t it fixed? Too much existing code depends on this behavior. Changing it now would break millions of websites. So we have to work around it.Workaround:
Copy
Ask AI
// Always check for null explicitlyfunction getType(value) { if (value === null) return "null"; return typeof value;}// Or check for "real" objectsif (value !== null && typeof value === "object") { // It's definitely an object (not null)}
Why? Arrays ARE objects in JavaScript. They inherit from Object.prototype and have special behavior for numeric keys and the length property, but they’re still objects.How to check for arrays:
Copy
Ask AI
Array.isArray([]) // true (recommended)Array.isArray({}) // falseArray.isArray("hello") // false// Or using Object.prototype.toStringObject.prototype.toString.call([]) // "[object Array]"
Why is this different? Functions are technically objects too, but typeof treats them specially because checking for “callable” values is so common. This is actually convenient!
Copy
Ask AI
// This makes checking for functions easyif (typeof callback === "function") { callback();}
typeof on Undeclared Variables
Copy
Ask AI
// Referencing an undeclared variable throws an errorconsole.log(undeclaredVar); // ReferenceError!// But typeof on an undeclared variable returns "undefined"typeof undeclaredVar // "undefined" (no error!)
Why? This was a design decision to allow safe feature detection:
Copy
Ask AI
// Safe way to check if a global existsif (typeof jQuery !== "undefined") { // jQuery is available}// vs. this would throw if jQuery doesn't existif (jQuery !== undefined) { // ReferenceError if jQuery not defined!}
In modern JavaScript with modules and bundlers, this pattern is less necessary. But it’s still useful for checking global variables and browser features.
NaN is a 'number' — Yes, Really
Copy
Ask AI
typeof NaN // "number"
“Not a Number” has a typeof of "number". This sounds strange, but NaN is actually a special value in the number system. It represents a calculation that doesn’t have a valid result, like 0 / 0.
Copy
Ask AI
// These all produce NaN0 / 0 // NaNparseInt("hello") // NaNMath.sqrt(-1) // NaN// Check for NaN properlyNumber.isNaN(NaN) // true
Since typeof has limitations, here are more reliable approaches:
Type-Specific Checks
instanceof
Object.prototype.toString
Custom Type Checker
Copy
Ask AI
// ArraysArray.isArray(value) // true for arrays only// NaNNumber.isNaN(value) // true for NaN only (no coercion)// Finite numbersNumber.isFinite(value) // true for finite numbers// IntegersNumber.isInteger(value) // true for integers// Safe integersNumber.isSafeInteger(value) // true for safe integers
These methods from Number are more reliable than typeof for numeric checks.
The instanceof operator checks if an object is an instance of a constructor:
Copy
Ask AI
// Check if an object is an instance of a constructor[] instanceof Array // true{} instanceof Object // truenew Date() instanceof Date // true/regex/ instanceof RegExp // true// Works with custom classesclass Person {}const p = new Person();p instanceof Person // true// Caveat: doesn't work across iframes/realms// The Array in iframe A is different from Array in iframe B
Default to === for all comparisons. It’s predictable, doesn’t perform type coercion, and will save you from countless bugs.The only exception: Use == null to check for both null and undefined in one comparison.
Why it’s wrong: Objects are compared by reference, not by value. Two objects with identical content are still different objects.The fix:
Copy
Ask AI
// Option 1: Compare specific propertiesif (user1.name === user2.name) { console.log("Same name!");}// Option 2: JSON.stringify (simple objects only)if (JSON.stringify(user1) === JSON.stringify(user2)) { console.log("Same content!");}// Option 3: Deep equality function or libraryimport { isEqual } from 'lodash';if (isEqual(user1, user2)) { console.log("Same content!");}
2. Truthy/Falsy Confusion with ==
The mistake:
Copy
Ask AI
// These all behave unexpectedlyif ([] == false) { } // true! (but [] is truthy)if ("0" == false) { } // true! (but "0" is truthy)if (" " == false) { } // false (but " " is truthy)
Why it’s confusing: The == operator doesn’t check truthiness. It performs type coercion according to specific rules.The fix:
Copy
Ask AI
// Use === for explicit comparisonsif (value === false) { } // Only true for actual false// Or check truthiness directlyif (!value) { } // Falsy checkif (value) { } // Truthy check// For explicit boolean conversionif (Boolean(value) === false) { }
3. NaN Comparisons
The mistake:
Copy
Ask AI
const result = parseInt("hello");if (result === NaN) { console.log("Not a number!"); // Never runs!}
Why it’s wrong:NaN is never equal to anything, including itself.The fix:
Copy
Ask AI
// Use Number.isNaN()if (Number.isNaN(result)) { console.log("Not a number!"); // Works!}// Or Object.is()if (Object.is(result, NaN)) { console.log("Not a number!"); // Works!}// Avoid isNaN() - it coerces firstisNaN("hello") // true (coerces to NaN)Number.isNaN("hello") // false (no coercion)
4. The typeof null Trap
The mistake:
Copy
Ask AI
function processObject(obj) { if (typeof obj === "object") { // Might be null! console.log(obj.property); // TypeError if null! }}processObject(null); // Crashes!
Why it’s wrong:typeof null === "object" is true due to a historical bug.The fix:
Copy
Ask AI
function processObject(obj) { // Check for null AND typeof if (obj !== null && typeof obj === "object") { console.log(obj.property); } // Or use optional chaining (ES2020) console.log(obj?.property);}
5. String Comparison Gotchas
The mistake:
Copy
Ask AI
// Comparing numbers as stringsconsole.log("10" > "9"); // false! (string comparison)// Why? Strings compare character by character// "1" (code 49) < "9" (code 57)
Why it’s wrong: String comparison uses lexicographic order (like a dictionary), not numeric value.The fix:
Copy
Ask AI
// Convert to numbers firstconsole.log(Number("10") > Number("9")); // trueconsole.log(+"10" > +"9"); // true (unary +)console.log(parseInt("10") > parseInt("9")); // true// For sorting arrays of number strings["10", "9", "2"].sort((a, b) => a - b); // ["2", "9", "10"]
6. Empty Array Comparisons
The mistake:
Copy
Ask AI
const arr = [];// These seem contradictoryconsole.log(arr == false); // trueconsole.log(arr ? "yes" : "no"); // "yes"// So arr equals false but is truthy?!
Why it’s confusing:== uses type coercion ([] → "" → 0), but truthiness just checks if the value is truthy (all objects are truthy).The fix:
Copy
Ask AI
// Check array length for "emptiness"if (arr.length === 0) { console.log("Array is empty");}// Or use the array itself as a boolean// (but remember, empty array is truthy!)if (!arr.length) { console.log("Array is empty");}
Misconception 1: '== is always bad and should never be used'
Not quite! While === should be your default, there’s one legitimate use case for ==:
Copy
Ask AI
// The one acceptable use of ==if (value == null) { // Catches both null AND undefined}// Equivalent to:if (value === null || value === undefined) { // Same result, but more verbose}
This is cleaner than checking for both values separately and is explicitly allowed by most style guides (including ESLint’s eqeqeq rule with the "null": "ignore" option).
Misconception 2: '=== checks if types are the same'
Partially wrong!=== doesn’t just check types. It checks if two values are the same type AND same value.
Copy
Ask AI
// Same type, different values → false5 === 10 // false (both numbers, different values)"hello" === "hi" // false (both strings, different values)// Different types → immediately false5 === "5" // false (no value comparison even attempted)
The key point: === returns false immediately if types differ, then compares values if types match.
Misconception 3: 'typeof is reliable for checking all types'
Wrong!typeof has several well-known quirks:
Copy
Ask AI
typeof null // "object" — famous bug from 1995!typeof [] // "object" — arrays are objectstypeof NaN // "number" — Not-a-Number is a number typetypeof function(){} // "function" — but functions ARE objects!
Better alternatives:
Use Array.isArray() for arrays
Use value === null for null
Use Number.isNaN() for NaN
Use Object.prototype.toString.call() for precise type detection
Misconception 4: 'Objects with the same content are equal'
Wrong! Objects (including arrays and functions) are compared by reference, not by content:
Copy
Ask AI
{ a: 1 } === { a: 1 } // false — different objects in memory![] === [] // false — different arrays!(() => {}) === (() => {}) // false — different functions!const obj = { a: 1 };obj === obj // true — same reference
To compare object contents, use JSON.stringify() for simple cases or a deep equality function like Lodash’s _.isEqual().
Misconception 5: 'NaN means the value is not a number'
Misleading!NaN is actually a numeric value that represents an undefined or unrepresentable mathematical result:
Copy
Ask AI
typeof NaN // "number" — NaN IS a number type!// NaN appears from invalid math operations0 / 0 // NaNMath.sqrt(-1) // NaNparseInt("xyz") // NaNInfinity - Infinity // NaN
Think of NaN as “the result of a calculation that doesn’t produce a meaningful number” rather than literally “not a number.”
Misconception 6: 'Truthy values are == true'
Wrong! Truthy/falsy and == equality are completely different concepts:
Copy
Ask AI
// These are truthy but NOT == true"hello" == true // false! ("hello" → NaN, true → 1)2 == true // false! (2 !== 1)[] == true // false! ([] → "" → 0, true → 1)// But they ARE truthyif ("hello") { } // executesif (2) { } // executesif ([]) { } // executes
Rule: Don’t use == true or == false. Either use === or just rely on truthiness directly: if (value).
This is why understanding type conversion is so important!
Question 2: Why does typeof null return 'object'?
Answer: This is a bug from JavaScript’s original implementation in 1995.In the original C code, values were represented with a type tag. Objects had the tag 000, and null was represented as the NULL pointer (0x00), which also matched the 000 tag for objects.This bug was never fixed because too much existing code depends on this behavior. A proposal to fix it was rejected for backward compatibility reasons.Workaround: Always check for null explicitly: value === null
Question 3: How would you properly check if a value is NaN?
Answer: Use Number.isNaN():
Copy
Ask AI
Number.isNaN(NaN) // trueNumber.isNaN("hello") // false (no coercion)Number.isNaN(undefined) // false
Avoid the global isNaN() because it coerces its argument first:
Copy
Ask AI
isNaN("hello") // true (coerces to NaN)isNaN(undefined) // true (coerces to NaN)
You can also use Object.is(value, NaN) which returns true for NaN.
Question 4: What's the ONE legitimate use case for ==?
Answer: Checking for both null and undefined in a single comparison:
Copy
Ask AI
// Using ==if (value == null) { // value is null OR undefined}// Equivalent to:if (value === null || value === undefined) { // value is null OR undefined}
This works because null == undefined is true (special case in the spec), but null and undefined don’t loosely equal anything else (not even 0, "", or false).
Question 5: Why does {} === {} return false?
Answer: Objects are compared by reference, not by value.When you write {}, JavaScript creates a new object in memory. When you write another {}, it creates a completely different object. Even though they have the same content (both are empty objects), they are stored at different memory locations.
Copy
Ask AI
const a = {};const b = {};const c = a;a === b // false (different objects)a === c // true (same reference)
To compare objects by content, you need to compare their properties or use a deep equality function.
Question 6: What's the difference between === and Object.is()?
Answer: They behave identically except for two edge cases:
Expression
===
Object.is()
NaN, NaN
false
true
+0, -0
true
false
Copy
Ask AI
NaN === NaN // falseObject.is(NaN, NaN) // true+0 === -0 // trueObject.is(+0, -0) // false
Use === for everyday comparisons. Use Object.is() when you specifically need to check for NaN equality or distinguish positive from negative zero.
Question 7: How would you reliably check if something is an array?
Why not typeof? Because typeof [] === "object". Arrays are objects in JavaScript.Why not instanceof Array? It works in most cases, but can fail across different JavaScript realms (like iframes) where each realm has its own Array constructor.
What is the difference between == and === in JavaScript?
The == (loose equality) operator converts values to the same type before comparing, while === (strict equality) compares both type and value without any conversion. For example, 5 == "5" is true because the string is coerced to a number, but 5 === "5" is false because a number and string are different types. According to the ECMAScript specification, === should be your default choice for reliable comparisons.
Why does NaN not equal itself in JavaScript?
This behavior comes from the IEEE 754 floating-point specification, which JavaScript follows. NaN represents an undefined or unrepresentable mathematical result (like 0/0 or Math.sqrt(-1)), so it cannot meaningfully equal any value — including itself. Use Number.isNaN() to check for NaN reliably.
When should I use == instead of === in JavaScript?
The only widely accepted use case for == is checking for null or undefined in a single expression: if (value == null). This catches both null and undefined because the ECMAScript specification defines null == undefined as true. Most linters, including ESLint’s eqeqeq rule, allow this specific pattern while enforcing === everywhere else.
What is Object.is() and when should I use it?
Object.is() is an ES6 method that implements the “SameValue” equality algorithm. It behaves like === except in two cases: Object.is(NaN, NaN) returns true (unlike ===), and Object.is(+0, -0) returns false (unlike ===). Use it when you need to distinguish positive and negative zero or reliably detect NaN.
Why does typeof null return 'object' in JavaScript?
This is a well-known bug from JavaScript’s first implementation in 1995. As documented by MDN, values in the original engine used type tags, and both objects and null shared the 000 tag. A TC39 proposal to fix this was rejected because changing it would break backward compatibility with millions of existing websites. Always check for null explicitly with value === null.