Skip to main content
This book is currently a work in progress. Content is being actively developed.

Chapter 4: Coercing Values

We’ve thoroughly covered all of the different types of values in JS. And along the way, more than a few times, we mentioned the notion of converting — actually, coercing — from one type of value to another. In this chapter, we’ll dive deep into coercion and uncover all its mysteries.

Coercion: Explicit vs Implicit

Some developers assert that when you explicitly indicate a type change in an operation, this doesn’t qualify as a coercion but just a type-cast or type-conversion. In other words, the claim is that coercion is only implicit. I disagree with this characterization. I use coercion to label any type conversion in a dynamically-typed language, whether it’s plainly obvious in the code or not.
The line between explicit and implicit is not clear and objective — it’s fairly subjective. Keep that subjectivity in mind as we explore various forms of coercion.

Implicit: Bad or …?

An extremely common opinion among JS developers is that coercion is bad, specifically, that implicit coercion is bad. The rise in popularity of type-aware tooling like TypeScript speaks loudly to this sentiment.
This feeling is not new. 14+ years ago, Douglas Crockford’s book “The Good Parts” famously decried implicit coercion as one of the bad parts. Even Brendan Eich, creator of JS, regularly claims that implicit coercion was a mistake in the early design of the language that he now regrets. However, here’s an observation I’ve made over the years: most of the folks who publicly condemn implicit coercion, actually use implicit coercion in their own code.
Crockford says to avoid implicit coercion, but his code uses if (..) statements with non-boolean values evaluated.Many dismiss this, claiming that conversion-to-boolean isn’t really coercion. But it absolutely is!
Brendan Eich says he regrets implicit coercion, but yet he openly endorses idioms like x + "" to coerce the value in x to a string. That’s most definitely an implicit coercion.
In fact, I think you’d be hard pressed to name any other well-known source of JS teaching that strongly endorses coercion (in virtually all its forms). I do — and this book definitely does! — but I feel mostly like a lone voice shouting futilely in the wilderness.

Abstracts

Now that I’ve challenged you to examine coercion in more depth, let’s first look at the foundations of how coercion occurs, according to the JS specification. The specification details a number of abstract operations that dictate internal conversion from one value-type to another.
These operations look as if they’re real functions that could be called, such as ToString() or ToNumber(). But by abstract, we mean they only exist conceptually by these names; they aren’t functions we can directly invoke in our programs.

ToBoolean

Decision making (conditional branching) always requires a boolean true or false value. But it’s extremely common to want to make these decisions based on non-boolean value conditions. When non-boolean values are encountered in a context that requires a boolean — such as the condition clause of an if statement — the ToBoolean() abstract operation is activated.
1

Falsy Values

These values coerce to false:
  • undefined
  • null
  • "" (empty string)
  • 0 and -0
  • 0n (bigint zero)
  • NaN
2

Truthy Values

Any other value that’s not in the falsy list is truthy and coerces to true:
  • "hello"
  • 42
  • [ 1, 2, 3 ] (even empty arrays [])
  • { a: 1 } (even empty objects {})
  • " " (string with only whitespace)
// Abstract ToBoolean() (conceptual)
ToBoolean(undefined);               // false
ToBoolean(null);                    // false
ToBoolean("");                      // false
ToBoolean(0);                       // false
ToBoolean(NaN);                     // false

ToBoolean("hello");                 // true
ToBoolean(42);                      // true
ToBoolean([]);                      // true (even empty!)
ToBoolean({});                      // true (even empty!)
The ToBoolean() coercion operation is basically a lookup table rather than an algorithm. Thus, some developers assert that this isn’t really coercion. I think that’s bogus — it converts from non-boolean value-types to a boolean, and that’s clear cut type coercion.

ToPrimitive

Any value that’s not already a primitive can be reduced to a primitive using the ToPrimitive() abstract operation. Generally, it’s given a hint to tell it whether a number or string is preferred.
// Abstract ToPrimitive() (conceptual)
ToPrimitive({ a: 1 }, "string");          // "[object Object]"
ToPrimitive({ a: 1 }, "number");          // NaN
The operation looks on the object for either a toString() method or a valueOf() method. The order it looks for those is controlled by the hint:
  • "string" hint → check toString() then valueOf()
  • "number" hint (or no hint) → check valueOf() then toString()
If the method returns a value matching the hinted type, the operation is finished. Otherwise, it tries the other method.

ToString

Pretty much any value that’s not already a string can be coerced to a string representation, via ToString():
// Abstract ToString() (conceptual)
ToString(42.0);                 // "42"
ToString(-3);                   // "-3"
ToString(Infinity);             // "Infinity"
ToString(NaN);                  // "NaN"
ToString(42n);                  // "42"

ToString(true);                 // "true"
ToString(false);                // "false"

ToString(null);                 // "null"
ToString(undefined);            // "undefined"
Some results may vary from common intuition:
ToString(Number.MAX_VALUE);     // "1.7976931348623157e+308"
ToString(-0);                   // "0" (hides the negative!)
ToString(Symbol("ok"));         // TypeError exception thrown

Default toString()

When ToString() is activated with an object value-type, it delegates to ToPrimitive():
ToString(new String("abc"));        // "abc"
ToString(new Number(42));           // "42"

ToString({ a: 1 });                 // "[object Object]"
ToString([ 1, 2, 3 ]);              // "1,2,3"

ToNumber

Non-number values that resemble numbers can generally be coerced to a numeric representation, using ToNumber():
// Abstract ToNumber() (conceptual)
ToNumber("42");                     // 42
ToNumber("-3");                     // -3
ToNumber("1.2300");                 // 1.23
ToNumber("   8.0    ");             // 8 (whitespace stripped)

ToNumber("123px");                  // NaN (not fully numeric)
ToNumber("hello");                  // NaN

ToNumber(true);                     // 1
ToNumber(false);                    // 0

ToNumber(null);                     // 0
ToNumber(undefined);                // NaN
There are some rather surprising designations:
ToNumber("");                       // 0 (surprising!)
ToNumber("       ");                // 0 (surprising!)
I think it would have made much more sense for these to coerce to NaN, the way undefined does.
Some primitive values are not allowed to be coerced to numbers:
ToNumber(42n);                      // TypeError exception thrown
ToNumber(Symbol("42"));             // TypeError exception thrown

Default valueOf()

When ToNumber() is activated on an object value-type, it delegates to ToPrimitive() with "number" as the hint:
ToNumber(new String("abc"));        // NaN
ToNumber(new Number(42));           // 42

ToNumber({ a: 1 });                 // NaN
ToNumber([ 1, 2, 3 ]);              // NaN
ToNumber([]);                       // 0 (empty array!)

Equality Comparison

When JS needs to determine if two values are the same value, it activates various equality comparison operations.
The strictest equality comparison, with absolutely no coercion. Used by Object.is().
SameValue(42, 42);                  // true
SameValue(NaN, NaN);                // true
SameValue(0, -0);                   // false
Like SameValue(), but treats 0 and -0 as indistinguishable. Used by Array.includes(), Set.has(), Map.has().
SameValueZero(0, -0);               // true
SameValueZero(NaN, NaN);            // true
Used by the === operator. Similar to SameValue(), but:
  • NaN === NaN returns false (unlike SameValue)
  • -0 === 0 returns true (unlike SameValue)
Used by the == operator. If types match, delegates to IsStrictlyEqual(). If types differ, performs coercion (preferring numeric comparison) until types match.

Higher-Abstracted Equality

The IsLooselyEqual() operation for the == operator:
1

Same Types

If types match, immediately delegates to IsStrictlyEqual() (same as ===).
2

Nullish Equality

If one value is null and the other is undefined, returns true. They’re coercively equal to each other (and to no other values).
3

Numeric Coercion

If types differ, prefers to coerce both operands to numbers:
  • If one is number and other is string, coerce string to number
  • If one is bigint and other is string, coerce string to bigint
  • If one is boolean, coerce it to number
  • If one is non-primitive (object), coerce it to primitive with ToPrimitive()
4

Recursive

Each time a coercion is performed, the algorithm is recursively reactivated with the new value(s), until the types are the same.

Relational Comparison

When values are compared relationally (“is one value less than another?”), the IsLessThan() abstract operation is activated:
// Abstract IsLessThan() (conceptual)
IsLessThan(1, 2, /*LeftFirst=*/ true);            // true
There is no IsGreaterThan() operation. Instead, the arguments to IsLessThan() can be reversed. The third argument (LeftFirst) preserves left-to-right evaluation semantics.
Like IsLooselyEqual(), the IsLessThan() operation is coercive, ensuring that value-types match before comparison (preferring numeric comparisons).

Concrete Coercions

Now that we’ve covered all the abstract operations JS defines for handling various coercions, it’s time to turn our attention to the concrete statements/expressions we can use in our programs that activate these operations.

To Boolean

To coerce a value that’s not of type boolean, we need the abstract ToBoolean() operation.
The most explicit way to activate ToBoolean():
Boolean("hello");               // true
Boolean(42);                    // true
Boolean("");                    // false
Boolean(0);                     // false
More common among JS developers:
!!"hello";                      // true
!!42;                           // true
!!"";                           // false
!!0;                            // false
The !! is two usages of the unary ! operator. The first ! coerces and negates, the second ! flips it back.
Statements like if, while, for, and ternary ? : implicitly coerce to boolean:
specialNumber = 42;

if (specialNumber) {
    // implicitly coerces to boolean
}
For both operators, the lefthand expression is coerced to boolean for the decision:
isLoggedIn = user.sessionID || req.cookie["Session-ID"];
isAdmin = isLoggedIn && ("admin" in user.permissions);
Note: Neither operator’s final result is actually coerced to a boolean. They return one of their operands.
Which is more explicit?Boolean(..) directly states what it’s doing: converting to boolean. However, !! is more recognizable to JS developers. The “most explicit” depends on your perspective.My take: Boolean(..) is the most preferable explicit coercion form. I think !!, if, for, while, &&, and || are all implicitly coercing non-booleans, but I’m OK with that.

To String

There are several ways to activate the ToString() coercion.
1

String() Function

The most explicit way:
String(true);                   // "true"
String(42);                     // "42"
String(-0);                     // "0"
String(Infinity);               // "Infinity"
String(null);                   // "null"
String(undefined);              // "undefined"

String(Symbol("ok"));           // "Symbol(ok)" (allowed explicitly!)
2

toString() Method

Available on non-nullish primitives via auto-boxing:
true.toString();                // "true"
42..toString();                 // "42"
-0..toString();                 // "0"
Symbol("ok").toString();        // "Symbol(ok)"

[1,2,3].toString();             // "1,2,3"
({ a: 1 }).toString();          // "[object Object]"
3

String Concatenation (+)

The + operator is overloaded to prefer string concatenation if either operand is already a string:
true + "";                      // "true"
42 + "";                        // "42"
null + "";                      // "null"
undefined + "";                 // "undefined"

Symbol("ok") + "";              // TypeError! (implicit coercion disallowed)
String Concatenation vs String() Function:A common misconception is that String(x) and x + "" are basically equivalent. They’re not!
  • String(Symbol("ok")) works (explicit coercion allowed)
  • Symbol("ok") + "" throws TypeError (implicit coercion disallowed)
JS intentionally discriminates between explicit and implicit symbol-to-string coercions. TC39 felt that symbols could too easily be mistaken as strings, so they wanted developers to express intent explicitly.
Brendan Eich endorses + "" as the best way to coerce values to strings. That carries weight in terms of him supporting at least a subset of implicit coercion practices!

To Number

Numeric coercions are a bit more complicated since we can target either number or bigint.
Number("42");                   // 42
Number("-3.141596");            // -3.141596
Number("-0");                   // -0

BigInt("42");                   // 42n
BigInt("-0");                   // 0n (no negative zero for bigint)

Number("123px");                // NaN
BigInt("123px");                // SyntaxError

Number(true);                   // 1
BigInt(true);                   // 1n

Number(42n);                    // 42
BigInt(42);                     // 42n
Commonly assumed to coerce the same as Number(), but has differences:
+"42";                          // 42
+"0b101010";                    // 42

// But watch out:
+42n;                           // TypeError! (implicit coercion)
Number(42n);                    // 42 (explicit coercion)
JS interprets unary + in front of a bigint as an implicit coercion (disallowed), but Number(..) as an explicit coercion (allowed).
Mathematical operators coerce non-number operands to numbers:
x - 0;                  // Coerces x to number
x * 1;                  // Coerces x to number
x / 1;                  // Coerces x to number
Note: x + 0 isn’t as safe, since + is overloaded for string concatenation.
Bitwise operators expect 32-bit integer operands:
x | 0;                  // Coerces x to 32-bit integer
This is one of several “type annotations” from ASM.js efforts.
Number(..) and + are NOT interchangeable:
Number(42n);                    // 42 (works)
+42n;                           // TypeError (fails)
I think Number(..) is the safer/more reliable form.

To Primitive

Most operators in JS are designed to run against primitive values. When used against an object value, the abstract ToPrimitive() algorithm is activated. Let’s set up a spy object to inspect how different operations behave:
spyObject = {
    toString() {
        console.log("toString() invoked!");
        return "10";
    },
    valueOf() {
        console.log("valueOf() invoked!");
        return 42;
    },
};
String(spyObject);
// toString() invoked!
// "10"

spyObject + "";
// valueOf() invoked!
// "42"
Surprise! String(..) and + "" are NOT equivalent:
  • String(..) provides "string" hint to ToPrimitive()
  • + "" provides no hint (similar to "number" hint)
Even though + "" invokes valueOf() returning 42, that value is then coerced to string "42".
Number(spyObject);
// valueOf() invoked!
// 42

+spyObject;
// valueOf() invoked!
// 42
Both perform the same ToPrimitive() coercion with "number" hint.
BigInt(spyObject);
// valueOf() invoked!
// 42n
If valueOf() returns a number, it can safely be coerced to bigint.

Overriding ToPrimitive

You can override the whole default ToPrimitive() operation by setting the special symbol property Symbol.toPrimitive:
spyObject6 = {
    [Symbol.toPrimitive](hint) {
        console.log(`toPrimitive(${hint}) invoked!`);
        return 25;
    },
    toString() {
        console.log("toString() invoked!");
        return "10";
    },
    valueOf() {
        console.log("valueOf() invoked!");
        return 42;
    },
};

String(spyObject6);
// toPrimitive(string) invoked!
// "25"   (not "10")

Number(spyObject6);
// toPrimitive(number) invoked!
// 25     (not 42)
If the defined Symbol.toPrimitive function does not return a primitive value, an exception will be thrown. Make sure to always return an actual primitive value!

Equality

The most obvious place where coercion is involved in equality checks is with the == operator.

Equality Operators: == vs ===

The == operator behaves extremely predictably, ensuring that both operands match types before performing its equality check.
1

Same Types

If the types of both operands are the same, == has the exact same behavior as ===. It immediately delegates to IsStrictlyEqual().
myObj = { a: 1 };
anotherObj = myObj;

myObj == anotherObj;                // true
myObj === anotherObj;               // true
2

Different Types

If the operand types differ, == allows coercion until they match, and prefers numeric comparison:
42 == "42";                         // true
Here, "42" string is coerced to 42 number (not vice versa), then the comparison is 42 == 42, which returns true.
Common Myth Dispelled:It’s often said that only === checks the type and value, while == checks only the value. Not true!Both == and === are type-sensitive, each checking the types of their operands. The difference:
  • == allows coercion of mismatched types
  • === disallows any coercion

Nullish Coercion

== is the most obvious place that JS exposes nullish coercive equality:
null == undefined;              // true
Neither null nor undefined will ever be coercively equal to any other value in the language, other than to each other.
if (someData == null) {
    // `someData` is "unset" (either null or undefined)
}

if (someData != null) {
    // `someData` is set (neither null nor undefined)
}
Compare these two approaches:
if (someData == null) {
    // ..
}

// vs:

if (someData === null || someData === undefined) {
    // ..
}
Both if statements behave identically. Which would you rather write and read?In performance benchmarks, the single == null check is slightly faster than the combination of two === checks. There’s a tiny but measurable benefit to letting JS’s == perform the implicit nullish coercion.

== Boolean Gotcha

The biggest gotcha to be aware of with == has to do with booleans.Pay very close attention here! If you take my simple advice (at the end of this section), you’ll never be a victim.
Consider this snippet, assuming isLoggedIn is NOT holding a boolean value:
if (isLoggedIn) {
    // ..
}

// vs:

if (isLoggedIn == true) {
    // ..
}
The first if statement activates ToBoolean() coercion. The second looks like it would behave the same way, but it doesn’t! The == operator doesn’t invoke ToBoolean(). When types don’t match, it prefers to coerce both to numbers. Example with isLoggedIn as "yes":
// (1) "yes" == true
// (2) "yes" == 1
// (3) NaN == 1
// (4) NaN === 1           // false
So if ("yes") passes, but if ("yes" == true) fails! Even worse:
// What value makes both forms pass?
if (isLoggedIn) { }         // passes when isLoggedIn is 1 or "1"
if (isLoggedIn == true) { } // passes when isLoggedIn is 1 or "1"
Simple advice to avoid this gotcha:Never, ever, under any circumstances, perform a == check if either side of the comparison is a true or false value.When dealing with booleans, stick to implicitly coercive forms that genuinely activate ToBoolean(), such as if (isLoggedIn).

Coercion Corner Cases

That’s not to say that coercion is perfect. There’s several frustrating corner cases we need to be aware of.

Strings

Array-to-string coercion is annoying:
String([1, 2, 3]);                // "1,2,3" (no brackets!)
String([]);                       // "" (can't tell it's an array!)
String([null, undefined]);        // "," (null/undefined disappear!)
Object-to-string coercion is almost as aggravating:
String({});                       // "[object Object]"
String({ a: 1 });                 // "[object Object]" (no help!)

Numbers

The worst root of all coercion corner case evil:
Number("");                       // 0
Number("       ");                // 0
I still shake my head at this one. The empty string has nothing in it to determine a numeric representation. 0 is NOT the numeric equivalent of missing/invalid numeric value. That’s what NaN is for!
Even worse, recall how [] coerces to the string ""? By extension:
Number([]);                       // 0 (absurd!)
Other mildly annoying cases:
Number("NaN");                    // NaN (accidental!)
Number("Infinity");               // Infinity
Number("infinity");               // NaN (case sensitive!)

Number(false);                    // 0
Number(true);                     // 1
false + true + false + false + true;  // 2 (really?)

Coercion Absurdity

To prove the point, let’s take absurdity to level 11:
[] == ![];                          // true
How can a value be coercively equal to its negation!? Follow down the coercion rabbit hole:
  1. [] == ![]
  2. [] == false (! coerces to boolean, negates)
  3. "" == false (array to string)
  4. 0 == false (empty string to number)
  5. 0 == 0 (boolean to number)
  6. 0 === 0true
Three different absurdities conspiring: String([]), Number(""), and Number(false). If any of these weren’t true, this nonsense wouldn’t occur.This is NOT ==’s fault. It gets the blame, but the real culprits are the underlying string and number corner cases.

Type Awareness

We’ve now examined coercion from every conceivable angle. But what’s the point of all this? Is it mostly just trivia? Let’s return to the observations I posed at the beginning of this chapter.
Key Insight:No matter what you do, you won’t be able to get away from the need to be aware of, understand, and manage JS’s value-types and the conversions between them.Type-aware programming is always, always better than type ignorant/agnostic programming.

Uhh… TypeScript?

Surely you’re thinking: “Why can’t I just use TypeScript and declare all my types statically, avoiding all the confusion of dynamic typing and coercion?”
TypeScript is both statically-typed (types declared at author time, checked at compile-time) and strongly-typed (variables/containers are typed, associations enforced, disallows implicit coercion).The greatest strength of TypeScript is that it typically forces both the author and reader of code to confront the types comprising a program.
  • TypeScript types are erased by the compiler
  • What’s left is just JS that the JS engine has to contend with
  • You cannot fully rely on TypeScript types to solve all problems
  • TypeScript can’t understand some specific situations
  • False positives: complaints about things which aren’t actually errors

Type-Awareness Without TypeScript

Does a dynamically-typed system automatically mean you’re programming with less type-awareness? I disagree. Consider this variable declaration:
let API_BASE_URL = "https://some.tld/api/2";
Is that statement in any way type-aware? Sure, there’s no : string annotation. But I definitely think it IS still type-aware! We clearly see the value-type (string) of the value being assigned. Later usage:
// Are we using the secure API URL?
isSecureAPI = /^https/.test(API_BASE_URL);
I know the regular-expression test() method expects a string, and since I know API_BASE_URL is holding a string, I know that operation is type-safe. Similarly, since I know the simple rules of ToBoolean() coercion:
// Do we have an API URL determined yet?
if (API_BASE_URL) {
    // ..
}
Type-awareness in coding means thinking carefully about whether such things will be clear and obvious to the reader of the code.

Type Aware Equality

Let’s revisit equality comparisons (== vs ===) from the perspective of type-awareness. I promised I would make the case for == over ===, so here it goes.
1

Known Facts

  1. If the types of operands for == match, it behaves exactly the same as ===
  2. If the types of operands for === do NOT match, it will always return false
  3. If the types of operands for == do NOT match, it will allow coercion (preferring numeric type-values) until types match
2

Possible Conditions

When comparing x and y, we might be in:
  1. Known types - We know exactly what type(s) x and y could be
  2. Unknown types - We can’t tell what those types could be
Condition (1) is far preferable and represents type-aware code. Condition (2) represents type-unaware code.

(2) Unknown Types

If you’re in scenario (2), your code is in a problem state. The best thing to do is… fix it! Change the code so it’s type-aware. If you cannot ensure type-awareness, you must use the === strict-equality operator:
if (x === y) {
    // ..
}
You’re only picking === as a last resort, when your code is so type-unaware (type-broken!) as to have no other choice.

(1) Known Types

If you know the types of x and y, there are two sub-conditions:
  • (1a) x and y are of the same type
  • (1b) x and y are of different types
(1a) Known Matching Types
If the types match, we know for certain that == and === do exactly the same thing. Except, == is shorter by one character.
// This is best
if (x == y) {
    // ..
}
An extra = would do nothing to make the code more clear. In fact, it makes it worse!
// This is strictly worse here!
if (x === y) {
    // ..
}
Why is it worse? Because === signals to readers that we’re avoiding coercion. But we already know that no coercion would occur! The === might confuse readers, making them second-guess their understanding.
If you know the types of an equality comparison, and you know they match, there’s only one right choice: ==.
(1b) Known Mismatched Types
We need to compare x and y, and we know their types are NOT the same. If you pick ===, you’ve made a huge mistake. Why? Because === used with known-mismatched types will never return true. It will always fail.
// `x` and `y` have different types?
if (x === y) {
    // Congratulations, this code will NEVER run
}
So === is out. We have two options:
  • (1b-1) Change the code so we’re not trying to compare known mismatched types (explicitly coerce one or both values so types match, then use ==)
  • (1b-2) If we’re going to compare known mismatched types for equality, we must use ==, because it’s the only operator that can coerce operands until types match
// `x` and `y` have different types,
// so let's allow JS to coerce them for equality comparison
if (x == y) {
    // .. (so, you're saying there's a chance?)
}

Summarizing Type-Sensitive Equality Comparison

The Case for == Over ===

The case for always preferring == over ===:
  1. Whether you use TypeScript or not, the goal should be to have every part of code be type-aware
  2. If you know the types, you should always prefer ==:
    • When types match, == is both shorter and more proper
    • When types don’t match, == is the only operator that can coerce operands, so it’s the only way such a check could ever hope to pass
  3. Only if you can’t know/predict the types, fall back to === as a last resort (and probably add a comment explaining why)
TypeScript’s Inconsistency Problem:If you’re using TypeScript properly and you know the types of an equality comparison, using === for that comparison is just plain wrong.The problem is, TypeScript strangely and frustratingly still requires you to use ===, unless it already knows that the types are matched.Try this in TypeScript:
let result = (42 == "42");
// This condition will always return 'false' since
// the types 'number' and 'string' have no overlap.
TypeScript is telling a lie here. Of course 42 == "42" produces true in JS!

Summary

We’ve covered coercion from every angle - the abstract operations, concrete forms, corner cases, and type-aware usage.

Key Takeaways

  • Coercion is unavoidable in JS
  • Type-awareness is crucial
  • == is preferable when types are known
  • Corner cases exist but can be avoided

Best Practices

  • Know your types
  • Use == for type-aware comparisons
  • Avoid == true and == false
  • Embrace coercion responsibly
The controversial conclusion:With proper understanding, JavaScript’s type system and coercion become powerful tools rather than sources of bugs. The key is not avoiding coercion, but understanding it deeply and using it wisely.

Build docs developers (and LLMs) love