Module: JavaScript
JavaScript·045·5 MIN READ

045: Type Conversion, Coercion, and Equality

TOPICS COVERED: 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:

js
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

js
Number("42");      // 42
Number(" 42 ");    // 42
Number("");        // 0
Number("42px");    // NaN

For parsing numbers that may contain additional text, parseInt() and parseFloat() behave differently:

js
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

js
String(42);        // "42"
String(true);      // "true"
String(null);      // "null"

Template literals perform string conversion during interpolation:

js
const quantity = 3;
const message = `Quantity: ${quantity}`;

To a boolean

Falsy values include:

js
false
0
-0
0n
""
null
undefined
NaN

Most other values are truthy, including:

js
"0"
"false"
[]
{}

This is a common interview and production-debugging area.

Implicit Coercion

The + operator is special

js
1 + 2;       // 3
"1" + 2;     // "12"
1 + "2";     // "12"

If string concatenation becomes involved, the result can surprise you.

A common form bug:

html
<input id="quantity" value="2">
<input id="price" value="100">
js
const quantity = document.querySelector("#quantity").value;
const price = document.querySelector("#price").value;

console.log(quantity + price); // "2100"

Fix the boundary:

js
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.

js
typeof NaN; // "number"

Do not test it with equality:

js
NaN === NaN; // false

Use:

js
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.

js
5 === 5;   // true
5 === "5"; // false

Use === and !== as the normal default.

==

Loose equality applies coercion rules.

js
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:

js
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:

js
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.

js
{ 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.

js
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:

js
[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:

js
const price = {
  valueOf() {
    return 250;
  },
};

console.log(price + 50); // 300

For string-oriented conversion, toString() may participate:

js
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:

js
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

SituationPrefer
ordinary application equality=== / !==
need NaN equal to itself and distinguish -0Object.is()
membership with includes(), Set, Mapunderstand SameValueZero
intentionally match only null or undefined togethervalue == null only if project convention permits it
compare object contentswrite domain-specific comparison

Relational comparisons also coerce

js
"20" < 100;   // true
"20" < "100"; // false

The first comparison can become numeric. The second compares strings lexicographically.

Normalize boundary data first:

js
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

js
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

js
function normalizeDiscount(value) {
  return value || 10;
}

console.log(normalizeDiscount(0)); // 10 — wrong if 0 is valid

Use nullish coalescing when zero is meaningful:

js
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 || when 0, false, or "" are valid values;
  • checking NaN with equality;
  • reaching for == because it "fixes" a type mismatch instead of fixing the boundary.

When debugging a suspicious value, inspect both value and type:

js
console.log({
  value,
  type: typeof value,
});

Best Practices

  • Convert external input deliberately.
  • Validate after conversion.
  • Default to === and !==.
  • Use Number.isNaN() and Number.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:

js
"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:

  • page to a positive integer;
  • includeArchived to a boolean based on "true" or "false";
  • missing sort to "newest".

Do not use loose equality.

Complete Solutions

js
function parseQuantity(value) {
  const quantity = Number(value);

  if (!Number.isInteger(quantity) || quantity <= 0) {
    throw new Error("Invalid quantity");
  }

  return quantity;
}
js
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 NaN needs 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