FullStack Course LogoFullStack Course
Module: JavaScript
JavaScript·045·9 MIN READ

045: Type Conversion, Coercion, and Equality

TOPICS COVERED: Type Conversion, Coercion, and Equality

Outcomes

By the end of this lesson, you should be able to:

  • 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 be comfortable with primitive values, objects, typeof, null, undefined, BigInt, and Symbol.

Before running these expressions, predict each result. The point is not just to get the answer right; it is to identify which conversions JavaScript is performing:

js
Number("42");
String(42);
Boolean(0);
Boolean("0");

Mental Model: Conversion Is a Boundary Decision

JavaScript will sometimes convert a value on your behalf. That can be convenient inside a small expression, but it becomes a common source of bugs when data crosses a boundary, such as a form, URL parameters, JSON, storage, or an API payload. Those sources often provide text even when the application concept is numeric or boolean.

The useful rule is to treat conversion as an explicit design decision whenever the expected type matters. Convert and validate at the boundary, then let the rest of the code work with a stable type.

Explicit Type Conversion

Explicit conversion is code that directly asks JavaScript to produce a value of another type. The conversion is visible at the point where it happens, which makes the boundary easier to inspect and test.

To a number

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

Number() attempts to interpret the entire value as a number. Whitespace is accepted, an empty string becomes 0, and a value such as "42px" is not a complete numeric representation, so the result is NaN.

When the input may contain additional text, parseInt() and parseFloat() have different behavior:

js
parseInt("42px", 10); // 42
parseFloat("12.50kg"); // 12.5
Number("42px");       // NaN

Do not select one by habit. Use parseInt() or parseFloat() when extracting a leading numeric portion is intentional. Use Number() when the entire input must be numeric, and validate the result afterward.

To a string

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

Template literals also convert interpolated values to strings:

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

That conversion is useful when building display text, but it does not change the original quantity variable into a string.

To a boolean

Boolean conversion is often used in conditionals, so this is where the terms truthy and falsy matter. The complete list of falsy values is:

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

Most other values are truthy, including values that can look empty or false when you are reading them as text:

js
"0"
"false"
[]
{}

An empty string is falsy, but the string "false" is non-empty and therefore truthy. Empty arrays and objects are objects, not falsy values. This distinction comes up frequently in interviews and production debugging because a value's appearance is not always the same as its boolean behavior.

Implicit Coercion

Implicit coercion is conversion caused by an operator or comparison rather than by an explicit call such as Number() or String(). It is part of JavaScript's rules, not random behavior, but several rules can interact in ways that are difficult to see in a code review.

The + operator is special

The plus operator can mean numeric addition or string concatenation:

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

Once string concatenation is involved, the result may not be the numeric calculation the caller intended. This is a particularly common problem with form controls, because an input's .value is a string even when the user typed digits.

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"

Both values are strings, so + concatenates them. Fix the boundary instead of relying on a later operation to coerce them:

js
const quantity = Number(document.querySelector("#quantity").value);
const price = Number(document.querySelector("#price").value);

console.log(quantity * price); // 200

The multiplication now operates on numbers. In real code, conversion should be followed by validation so invalid user input does not silently enter the calculation.

NaN

NaN means "Not-a-Number", although its JavaScript type is still number:

js
typeof NaN; // "number"

NaN represents an invalid numeric result. It has a deliberately unusual equality rule: it is not equal to itself.

js
NaN === NaN; // false

Do not use equality to test for it. Use Number.isNaN():

js
Number.isNaN(NaN);        // true
Number.isNaN("not a number"); // false

The global isNaN() function coerces its argument before testing. That makes it easier to misuse when the requirement is specifically to determine whether an existing value is the number NaN.

Loose and Strict Equality

===

Strict equality compares values without applying type coercion:

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

Use === and !== as the normal default. When a comparison unexpectedly fails, the difference between 5 and "5" remains visible instead of being hidden by an automatic conversion.

==

Loose equality applies coercion rules before comparing in many cases:

js
5 == "5";       // true
false == 0;     // true
"" == 0;        // true
null == undefined; // true

You still need to understand loose equality to read existing code and diagnose behavior. For new application code, avoiding it generally makes control flow easier to reason about.

There is one deliberate pattern you may encounter:

js
if (value == null) {
  // matches null or undefined
}

This condition matches exactly null or undefined, but not other falsy values. If a codebase uses this convention, document it so the intentional exception is clear. Otherwise, prefer explicit checks.

Object.is()

Object.is() mostly behaves like strict equality, with two differences that matter for edge cases:

js
Object.is(NaN, NaN); // true
Object.is(0, -0);    // false

NaN === NaN; // false
0 === -0;    // true

Use Object.is() when treating NaN as equal to itself or distinguishing positive zero from negative zero is part of the domain or algorithm. For ordinary application comparisons, === is still the clearer default.

Equality of Objects

Objects compare by identity, not by shape or by the properties they happen to contain:

js
{ id: 1 } === { id: 1 }; // false

const first = { id: 1 };
const second = first;

first === second; // true

The two object literals have the same property, but they are two different object instances. first and second refer to the same instance, so that comparison is true.

If the application needs to compare object content, define what equality means for that domain rather than assuming === performs a deep comparison:

js
function sameProduct(a, b) {
  return a.id === b.id && a.sku === b.sku;
}

SameValueZero

Not every JavaScript API uses strict equality. Collections such as Set, and membership operations such as Array.prototype.includes(), use an equality algorithm commonly described as SameValueZero.

One practical consequence is that NaN can match itself in these APIs, and duplicate NaN values occupy one Set entry:

js
[NaN].includes(NaN); // true

const values = new Set([NaN, NaN]);
console.log(values.size); // 1

You do not need to memorize the specification algorithm name immediately. You do need to remember that comparison behavior depends on the API you are using; ===, Object.is(), includes(), Set, and Map are not interchangeable comparison contexts.

Deep Dive: How Objects Become Primitives

An operator may require a primitive value but receive an object instead. In that situation, JavaScript applies the abstract process often called ToPrimitive. You do not need to memorize the specification algorithm line by line, but you should understand that object coercion can invoke methods on the object.

For an ordinary object, valueOf() can provide the value used by numeric-looking operations:

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"

This behavior explains why coercion can execute code that is not obvious from the operator alone. Avoid designing business objects around surprising implicit conversion. An explicit method such as product.getDisplayName() usually communicates intent better.

Symbol.toPrimitive

An object can define its conversion behavior directly with Symbol.toPrimitive:

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 helps explain the platform and can be appropriate for specialized value objects, but overusing it makes an API harder to read. Explicit conversion or domain methods are generally easier for another developer to follow.

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

Relational operators have their own conversion behavior, so equality rules are not the only place where types matter:

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

The first comparison can become numeric. The second compares two strings lexicographically, character by character, so it does not mean the same thing as comparing their numeric values.

Normalize boundary data before making these comparisons:

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 throughout this lesson is simple: normalize once, then reason with stable types.

Worked Example: Normalize Checkout Input

Here conversion happens at the boundary of the checkout code. Once this function returns, callers can rely on the validated shapes and numeric types:

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);

Conversion occurs once at the boundary. The business logic can then work with predictable types instead of repeatedly guessing what each raw value represents. The example also shows why conversion and validation belong together: conversion produces a candidate value, while validation decides whether that value is acceptable.

Failure Example: Falsy Defaults

A truthiness fallback is not the same as a missing-value fallback:

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

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

|| uses the fallback for every falsy value, including the valid number 0. Use nullish coalescing when only null and undefined should trigger the default:

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

console.log(normalizeDiscount(0)); // 0

This distinction also matters for valid false and empty-string values. Choose the operator based on whether you mean “falsy” or “missing.”

Mistakes and Debugging

Common problems include:

  • 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 a value looks suspicious, inspect the value and its type together. The type often identifies the boundary where the bug was introduced:

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

If the value is NaN, inspect the conversion input next. If it is a string where the code expects a number, trace it back to the form, URL, storage, or API boundary rather than hiding the mismatch with loose equality.

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 each result before evaluating it, and explain which operation causes any conversion:

js
"5" + 1
"5" - 1
Boolean([])
Boolean("")
Number(null)
Number(undefined)

Practice

Write parseQuantity(value) so that it 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;
}

The conversion happens before the integer and positivity checks. That gives the function one predictable numeric value to validate and ensures invalid input fails at the boundary.

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 };
}

The boolean conversion is deliberately narrow: only the string "true" produces true; other values produce false. The page is converted and then validated, while the missing sort value uses ?? so the fallback is limited to null or undefined.

Recap

You should now be able to explain:

  • the difference between conversion and coercion;
  • the difference between strict and 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
Reader page: /javascript/lesson/045/type-conversion-coercion-and-equality