045: Type Conversion, Coercion, and Equality
Outcomes
By the end of this lesson, you can:
- distinguish explicit conversion from implicit coercion;
- predict common string, number, and boolean conversions;
- explain why
===is the normal default for application code; - use
Object.is()when its special equality semantics are useful; - understand the equality behavior used by
Map,Set, and array membership methods; - avoid bugs caused by falsy values,
NaN, and accidental string concatenation.
Prerequisites and Retrieval
You should already understand primitive values, objects, typeof, null, undefined, BigInt, and Symbol.
Predict these before running them:
Number("42");
String(42);
Boolean(0);
Boolean("0");
Mental Model: Conversion Is a Boundary Decision
JavaScript sometimes converts values for you. That convenience can become a source of bugs when input crosses a boundary: form values, URL parameters, JSON, storage, or API payloads.
Treat conversion as an explicit design decision whenever the expected type matters.
Explicit Type Conversion
To a number
Number("42"); // 42
Number(" 42 "); // 42
Number(""); // 0
Number("42px"); // NaN
For parsing numbers that may contain additional text, parseInt() and parseFloat() behave differently:
parseInt("42px", 10); // 42
parseFloat("12.50kg"); // 12.5
Number("42px"); // NaN
Do not choose between them by habit. Choose based on whether the entire input must be numeric.
To a string
String(42); // "42"
String(true); // "true"
String(null); // "null"
Template literals perform string conversion during interpolation:
const quantity = 3;
const message = `Quantity: ${quantity}`;
To a boolean
Falsy values include:
false
0
-0
0n
""
null
undefined
NaN
Most other values are truthy, including:
"0"
"false"
[]
{}
This is a common interview and production-debugging area.
Implicit Coercion
The + operator is special
1 + 2; // 3
"1" + 2; // "12"
1 + "2"; // "12"
If string concatenation becomes involved, the result can surprise you.
A common form bug:
<input id="quantity" value="2">
<input id="price" value="100">
const quantity = document.querySelector("#quantity").value;
const price = document.querySelector("#price").value;
console.log(quantity + price); // "2100"
Fix the boundary:
const quantity = Number(document.querySelector("#quantity").value);
const price = Number(document.querySelector("#price").value);
console.log(quantity * price); // 200
NaN
NaN means "Not-a-Number", but its type is still number.
typeof NaN; // "number"
Do not test it with equality:
NaN === NaN; // false
Use:
Number.isNaN(NaN); // true
Number.isNaN("not a number"); // false
The global isNaN() coerces first and is therefore easier to misuse.
Loose and Strict Equality
===
Strict equality compares without type coercion.
5 === 5; // true
5 === "5"; // false
Use === and !== as the normal default.
==
Loose equality applies coercion rules.
5 == "5"; // true
false == 0; // true
"" == 0; // true
null == undefined; // true
You should understand these rules to read existing code, but avoiding loose equality makes application behavior easier to reason about.
A deliberate exception sometimes seen is:
if (value == null) {
// matches null or undefined
}
If your codebase uses this convention, document it. Otherwise prefer explicit checks.
Object.is()
Object.is() mostly resembles strict equality, with two notable differences:
Object.is(NaN, NaN); // true
Object.is(0, -0); // false
NaN === NaN; // false
0 === -0; // true
This makes Object.is() useful when those edge cases matter.
Equality of Objects
Objects compare by identity, not by shape.
{ id: 1 } === { id: 1 }; // false
const first = { id: 1 };
const second = first;
first === second; // true
To compare object content, you need domain-specific comparison logic.
function sameProduct(a, b) {
return a.id === b.id && a.sku === b.sku;
}
SameValueZero
Collections such as Set and operations such as Array.prototype.includes() use an equality algorithm commonly described as SameValueZero.
A practical consequence:
[NaN].includes(NaN); // true
const values = new Set([NaN, NaN]);
console.log(values.size); // 1
You do not need to memorize specification algorithm names immediately, but you should know that not every JavaScript comparison API uses exactly the same equality rule.
Deep Dive: How Objects Become Primitives
When an operator needs a primitive but receives an object, JavaScript applies the abstract idea often called ToPrimitive. You do not need to memorize the specification algorithm line by line, but you should understand why object coercion can call methods.
For an ordinary object:
const price = {
valueOf() {
return 250;
},
};
console.log(price + 50); // 300
For string-oriented conversion, toString() may participate:
const product = {
name: "Tea",
toString() {
return this.name;
},
};
console.log(String(product)); // "Tea"
Do not design business objects around surprising implicit coercion. Explicit methods such as product.getDisplayName() are usually clearer.
Symbol.toPrimitive
An object can define its conversion behavior explicitly:
const money = {
amount: 500,
[Symbol.toPrimitive](hint) {
if (hint === "number") {
return this.amount;
}
return `₹${this.amount}`;
},
};
console.log(Number(money)); // 500
console.log(String(money)); // "₹500"
The hint can be "number", "string", or "default".
This is advanced language machinery. It is useful for understanding the platform and specialized value objects, but it can make APIs harder to read when overused.
Equality Decision Table
| Situation | Prefer |
|---|---|
| ordinary application equality | === / !== |
need NaN equal to itself and distinguish -0 | Object.is() |
membership with includes(), Set, Map | understand SameValueZero |
intentionally match only null or undefined together | value == null only if project convention permits it |
| compare object contents | write domain-specific comparison |
Relational comparisons also coerce
"20" < 100; // true
"20" < "100"; // false
The first comparison can become numeric. The second compares strings lexicographically.
Normalize boundary data first:
const min = Number(formData.get("min"));
const max = Number(formData.get("max"));
if (!Number.isFinite(min) || !Number.isFinite(max)) {
throw new Error("Invalid range");
}
console.log(min < max);
The safest rule is the same throughout this lesson: normalize once, then reason with stable types.
Worked Example: Normalize Checkout Input
function normalizeCheckoutInput(raw) {
const quantity = Number(raw.quantity);
const unitPrice = Number(raw.unitPrice);
const discount = Number(raw.discount ?? 0);
if (!Number.isInteger(quantity) || quantity <= 0) {
throw new Error("Quantity must be a positive integer");
}
if (!Number.isFinite(unitPrice) || unitPrice < 0) {
throw new Error("Unit price must be a non-negative number");
}
if (!Number.isFinite(discount) || discount < 0) {
throw new Error("Discount must be a non-negative number");
}
return {
quantity,
unitPrice,
discount,
};
}
const input = normalizeCheckoutInput({
quantity: "2",
unitPrice: "350.50",
discount: "",
});
console.log(input);
Notice that conversion happens once at the boundary. Business logic can then work with predictable types.
Failure Example: Falsy Defaults
function normalizeDiscount(value) {
return value || 10;
}
console.log(normalizeDiscount(0)); // 10 — wrong if 0 is valid
Use nullish coalescing when zero is meaningful:
function normalizeDiscount(value) {
return value ?? 10;
}
console.log(normalizeDiscount(0)); // 0
Mistakes and Debugging
Common problems:
- trusting form values to already be numbers;
- using
parseInt()when decimal precision matters; - using
Number()on partial numeric strings without validating the result; - comparing objects by shape with
===; - using
||when0,false, or""are valid values; - checking
NaNwith equality; - reaching for
==because it "fixes" a type mismatch instead of fixing the boundary.
When debugging a suspicious value, inspect both value and type:
console.log({
value,
type: typeof value,
});
Best Practices
- Convert external input deliberately.
- Validate after conversion.
- Default to
===and!==. - Use
Number.isNaN()andNumber.isFinite()for numeric validation. - Use
??when only missing values should trigger a fallback. - Keep domain comparisons explicit.
- Do not rely on clever coercion as an application design strategy.
Exercises
Core
Predict:
"5" + 1
"5" - 1
Boolean([])
Boolean("")
Number(null)
Number(undefined)
Practice
Write parseQuantity(value) that returns a positive integer or throws an error.
Professional Extension
Write normalizeSearchParams(searchParams) that converts:
pageto a positive integer;includeArchivedto a boolean based on"true"or"false";- missing
sortto"newest".
Do not use loose equality.
Complete Solutions
function parseQuantity(value) {
const quantity = Number(value);
if (!Number.isInteger(quantity) || quantity <= 0) {
throw new Error("Invalid quantity");
}
return quantity;
}
function normalizeSearchParams(searchParams) {
const page = Number(searchParams.get("page") ?? 1);
const includeArchived = searchParams.get("includeArchived") === "true";
const sort = searchParams.get("sort") ?? "newest";
if (!Number.isInteger(page) || page <= 0) {
throw new Error("Invalid page");
}
return { page, includeArchived, sort };
}
Recap
You should now be able to explain:
- conversion versus coercion;
- strict versus loose equality;
- why
NaNneeds special handling; - why object equality means identity;
- why
??differs from||; - why application boundaries are the best place to normalize types.
Official References
- MDN: Type coercion
- MDN: Equality comparisons and sameness
- MDN:
Number - MDN:
Object.is
