The ==
operator (loose equality) compares values with type coercion, while the ===
operator (strict equality) compares values and types without coercion:
5 == "5"; // true (converts string to number)
5 === "5"; // false (different types)
null == undefined; // true (special case)
null === undefined; // false (different types)
0 == false; // true (converts boolean to number)
0 === false; // false (different types)
Recommendation: Use ===
in 99% of cases for predictable behavior.
// Compares type AND value
console.log(5 === 5); // true
console.log(5 === "5"); // false (different types)
console.log(true === true); // true
console.log(true === 1); // false (different types)
console.log(null === null); // true
console.log(undefined === undefined); // true
console.log(null === undefined); // false
// Compares values with type coercion
console.log(5 == "5"); // true ("5" → 5)
console.log(true == 1); // true (true → 1)
console.log(false == 0); // true (false → 0)
console.log(null == undefined); // true (special case)
console.log("" == 0); // true ("" → 0)
Comparison | == (loose) | === (strict) | Explanation |
---|---|---|---|
5 == "5" | true | false | String is converted to number |
true == 1 | true | false | Boolean is converted to number |
false == 0 | true | false | Boolean is converted to number |
null == undefined | true | false | Special rule for == |
"" == 0 | true | false | Empty string is converted to 0 |
" " == 0 | true | false | Space is converted to 0 |
"0" == 0 | true | false | String is converted to number |
[] == 0 | true | false | Array is converted to string, then to number |
[] == "" | true | false | Array is converted to empty string |
[1] == 1 | true | false | Array is converted to string “1”, then to number |
===
// Type coercion examples
console.log("5" == 5); // "5" → 5, then 5 == 5 → true
console.log(true == "1"); // true → 1, "1" → 1, then 1 == 1 → true
console.log([1,2] == "1,2"); // [1,2] → "1,2", then "1,2" == "1,2" → true
⚠️ Warning! These comparisons can lead to unexpected results:
// Paradoxes with ==
console.log("" == 0); // true
console.log(" " == 0); // true
console.log("\n" == 0); // true
console.log("\t" == 0); // true
// Transitivity violated!
console.log("" == 0); // true
console.log(0 == false); // true
console.log("" == false); // true, but logically strange
// Arrays
console.log([] == 0); // true
console.log([] == false); // true
console.log([] == ""); // true
// ❌ Bad: unexpected behavior
function checkValue(value) {
if (value == null) {
return "Empty value"; // Will work for null AND undefined
}
return "Has value";
}
console.log(checkValue(null)); // "Empty value"
console.log(checkValue(undefined)); // "Empty value"
console.log(checkValue(0)); // "Has value"
console.log(checkValue("")); // "Has value"
// ✅ Good: explicit check
function checkValueStrict(value) {
if (value === null || value === undefined) {
return "Empty value";
}
return "Has value";
}
// ✅ Or use nullish coalescing
const result = value ?? "default value";
// ❌ Bad: unexpected results
function validateAge(input) {
if (input == 18) {
return "Adult";
}
return "Minor";
}
console.log(validateAge("18")); // "Adult" (works by accident)
console.log(validateAge(" 18 ")); // "Minor" (spaces!)
console.log(validateAge(true)); // "Minor" (true != 18)
// ✅ Good: explicit type conversion
function validateAgeStrict(input) {
const age = Number(input);
if (age === 18) {
return "Adult";
}
return "Minor";
}
// ❌ Bad: checkbox can be a string
function handleCheckbox(isChecked) {
if (isChecked == true) {
console.log("Checked");
}
}
handleCheckbox("true"); // Won't work!
handleCheckbox(1); // Will work (1 == true)
// ✅ Good: explicit check
function handleCheckboxStrict(isChecked) {
if (isChecked === true || isChecked === "true") {
console.log("Checked");
}
}
// ✅ Or convert to boolean
function handleCheckboxBoolean(isChecked) {
if (Boolean(isChecked)) {
console.log("Checked");
}
}
In 99% of cases — it’s the safe choice:
// ✅ Safe comparisons
if (user.age === 18) { /* ... */ }
if (status === "active") { /* ... */ }
if (count === 0) { /* ... */ }
if (value === null) { /* ... */ }
if (typeof data === "string") { /* ... */ }
// ✅ In arrays and objects
const users = ["admin", "user", "guest"];
if (users.includes("admin")) { /* ... */ } // includes uses ===
// ✅ In switch (uses ===)
switch (userRole) {
case "admin":
// ...
break;
}
Only when you know exactly what you’re doing:
// ✅ Check for null/undefined simultaneously
if (value == null) {
// Will work for null AND undefined
console.log("Value is empty");
}
// Equivalent to:
if (value === null || value === undefined) {
console.log("Value is empty");
}
// ✅ But better to use nullish coalescing
const result = value ?? "default";
console.log(0 == false);
console.log(0 === false);
console.log("" == false);
console.log("" === false);
true
, false
, true
, false
0 == false
→ false
is converted to 0
, then 0 == 0
→ true
0 === false
→ different types → false
"" == false
→ false
is converted to 0
, ""
is converted to 0
, then 0 == 0
→ true
"" === false
→ different types → false
function isAdult(age) {
return age == 18;
}
console.log(isAdult("18")); // ?
console.log(isAdult(" 18")); // ?
console.log(isAdult(18.0)); // ?
true
, false
, true
Problem: " 18"
(with space) is not equal to 18
even with type coercion.
Solution:
function isAdult(age) {
return Number(age) === 18;
}
const arr = [];
console.log(arr == 0);
console.log(arr == "");
console.log(arr == false);
console.log(arr === false);
true
, true
, true
, false
[]
is converted to empty string ""
0
false
is converted to 0
==
give true
===
compares types, so false
// ❌ Problematic function
function findUser(users, id) {
return users.find(user => user.id == id);
}
const users = [
{ id: 1, name: "Anna" },
{ id: "2", name: "Boris" },
{ id: 3, name: "Vera" }
];
console.log(findUser(users, "1")); // Will find Anna (1 == "1")
console.log(findUser(users, 2)); // Will find Boris ("2" == 2)
// ✅ Fixed function
function findUser(users, id) {
// Convert id to string for consistency
const searchId = String(id);
return users.find(user => String(user.id) === searchId);
}
// Or convert to number
function findUserById(users, id) {
const searchId = Number(id);
return users.find(user => Number(user.id) === searchId);
}
// Or use strict comparison with type check
function findUserStrict(users, id) {
return users.find(user => user.id === id);
}
function validateForm(data) {
// Find problems in this validation
if (data.name == "") {
return "Name is required";
}
if (data.age == 0) {
return "Age is required";
}
if (data.isActive == false) {
return "Account must be active";
}
return "Validation passed";
}
// Test data
console.log(validateForm({ name: 0, age: false, isActive: "" }));
Problems:
name: 0
will pass the check name == ""
(0 != "")age: false
will not pass the check age == 0
(false == 0 → true)isActive: ""
will not pass the check isActive == false
("" != false)Fixed version:
function validateForm(data) {
if (typeof data.name !== "string" || data.name.trim() === "") {
return "Name is required";
}
if (typeof data.age !== "number" || data.age <= 0) {
return "Age must be a positive number";
}
if (data.isActive !== true) {
return "Account must be active";
}
return "Validation passed";
}
// ✅ Good
if (status === "active") { /* ... */ }
if (count === 0) { /* ... */ }
if (user === null) { /* ... */ }
// ❌ Bad
if (status == "active") { /* ... */ }
if (count == 0) { /* ... */ }
if (user == null) { /* ... */ }
// ✅ Good: explicit conversion
const userInput = "25";
const age = Number(userInput);
if (age === 25) { /* ... */ }
// ❌ Bad: implicit conversion
if (userInput == 25) { /* ... */ }
// ✅ Good: check type
function processValue(value) {
if (typeof value === "string" && value.length > 0) {
return value.toUpperCase();
}
if (typeof value === "number" && value > 0) {
return value * 2;
}
return null;
}
// ❌ Bad: rely on type coercion
function processValueBad(value) {
if (value == true) {
return value.toUpperCase(); // May crash!
}
return value * 2;
}
// .eslintrc.json
{
"rules": {
"eqeqeq": ["error", "always"], // Forbids ==
"no-implicit-coercion": "error" // Forbids implicit coercion
}
}
// Object.is() is even stricter than ===
console.log(Object.is(NaN, NaN)); // true (=== gives false)
console.log(Object.is(-0, +0)); // false (=== gives true)
console.log(Object.is(5, 5)); // true
console.log(Object.is(5, "5")); // false
// Useful for special cases
function isActuallyNaN(value) {
return Object.is(value, NaN);
}
// Instead of checking for null/undefined
const result = value ?? "default";
// Instead of
const result2 = (value === null || value === undefined)
? "default"
: value;
// Safe property access
const city = user?.address?.city;
// Instead of
const city2 = user && user.address && user.address.city;
// ❌ Bad
if (isActive == true) { /* ... */ }
if (hasPermission == false) { /* ... */ }
// ✅ Good
if (isActive === true) { /* ... */ }
if (isActive) { /* ... */ } // Even better
if (!hasPermission) { /* ... */ } // For false
// ❌ Always false (references are compared)
console.log([] == []); // false
console.log({} == {}); // false
// ✅ Proper array comparison
function arraysEqual(a, b) {
return a.length === b.length &&
a.every((val, i) => val === b[i]);
}
// ✅ Proper object comparison
function objectsEqual(a, b) {
return JSON.stringify(a) === JSON.stringify(b); // Simple way
// Or use lodash.isEqual library
}
const obj = { count: 0, name: "" };
// ❌ Bad: 0 and "" are considered false
if (obj.count) { /* ... */ } // Won't work for 0
if (obj.name) { /* ... */ } // Won't work for ""
// ✅ Good: check existence
if ("count" in obj) { /* ... */ }
if (obj.hasOwnProperty("count")) { /* ... */ }
if (obj.count !== undefined) { /* ... */ }
Golden Rules:
===
in 99% of cases — it’s safe and predictable==
— it can lead to unexpected resultsNumber()
, String()
, Boolean()
typeof
, Array.isArray()
, instanceof
??
, ?.
, Object.is()
Remember: Code should be understandable not only to the computer, but also to other developers. Strict equality ===
makes your intentions clear and prevents errors.
Want more articles for interview preparation? Subscribe to EasyAdvice, bookmark the site and level up every day 💪