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.Implicit: Bad or …?
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.Douglas Crockford's Code
Douglas Crockford's 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's Endorsement
Brendan Eich's Endorsement
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.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 booleantrue 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.
Falsy Values
These values coerce to
false:undefinednull""(empty string)0and-00n(bigint zero)NaN
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 theToPrimitive() abstract operation. Generally, it’s given a hint to tell it whether a number or string is preferred.
How ToPrimitive Works
How ToPrimitive Works
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 → checktoString()thenvalueOf()"number"hint (or no hint) → checkvalueOf()thentoString()
ToString
Pretty much any value that’s not already a string can be coerced to a string representation, viaToString():
Default toString()
WhenToString() is activated with an object value-type, it delegates to ToPrimitive():
ToNumber
Non-number values that resemble numbers can generally be coerced to a numeric representation, usingToNumber():
Default valueOf()
WhenToNumber() is activated on an object value-type, it delegates to ToPrimitive() with "number" as the hint:
Equality Comparison
When JS needs to determine if two values are the same value, it activates various equality comparison operations.SameValue()
SameValue()
The strictest equality comparison, with absolutely no coercion. Used by
Object.is().SameValueZero()
SameValueZero()
Like
SameValue(), but treats 0 and -0 as indistinguishable. Used by Array.includes(), Set.has(), Map.has().IsStrictlyEqual()
IsStrictlyEqual()
Used by the
=== operator. Similar to SameValue(), but:NaN === NaNreturnsfalse(unlikeSameValue)-0 === 0returnstrue(unlikeSameValue)
IsLooselyEqual()
IsLooselyEqual()
Used by the
== operator. If types match, delegates to IsStrictlyEqual(). If types differ, performs coercion (preferring numeric comparison) until types match.Higher-Abstracted Equality
TheIsLooselyEqual() operation for the == operator:
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).Numeric Coercion
If types differ, prefers to coerce both operands to numbers:
- If one is
numberand other isstring, coercestringtonumber - If one is
bigintand other isstring, coercestringtobigint - If one is
boolean, coerce it tonumber - If one is non-primitive (object), coerce it to primitive with
ToPrimitive()
Relational Comparison
When values are compared relationally (“is one value less than another?”), theIsLessThan() abstract operation is activated:
There is no
IsGreaterThan() operation. Instead, the arguments to IsLessThan() can be reversed. The third argument (LeftFirst) preserves left-to-right evaluation semantics.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 typeboolean, we need the abstract ToBoolean() operation.
Boolean() Function
Boolean() Function
The most explicit way to activate
ToBoolean():Double-Bang Idiom (!!)
Double-Bang Idiom (!!)
More common among JS developers:The
!! is two usages of the unary ! operator. The first ! coerces and negates, the second ! flips it back.Conditional Statements
Conditional Statements
Statements like
if, while, for, and ternary ? : implicitly coerce to boolean:Logical Operators (&&, ||)
Logical Operators (&&, ||)
For both operators, the lefthand expression is coerced to boolean for the decision:Note: Neither operator’s final result is actually coerced to a boolean. They return one of their operands.
To String
There are several ways to activate theToString() coercion.
To Number
Numeric coercions are a bit more complicated since we can target eithernumber or bigint.
Number() and BigInt() Functions
Number() and BigInt() Functions
Unary + Operator
Unary + Operator
Commonly assumed to coerce the same as JS interprets unary
Number(), but has differences:+ in front of a bigint as an implicit coercion (disallowed), but Number(..) as an explicit coercion (allowed).Mathematical Operations
Mathematical Operations
Mathematical operators coerce non-number operands to numbers:Note:
x + 0 isn’t as safe, since + is overloaded for string concatenation.Bitwise Operations
Bitwise Operations
Bitwise operators expect 32-bit integer operands:This is one of several “type annotations” from ASM.js efforts.
To Primitive
Most operators in JS are designed to run against primitive values. When used against an object value, the abstractToPrimitive() algorithm is activated.
Let’s set up a spy object to inspect how different operations behave:
String Coercion with Objects
String Coercion with Objects
String(..) and + "" are NOT equivalent:String(..)provides"string"hint toToPrimitive()+ ""provides no hint (similar to"number"hint)
+ "" invokes valueOf() returning 42, that value is then coerced to string "42".Number Coercion with Objects
Number Coercion with Objects
ToPrimitive() coercion with "number" hint.BigInt Coercion with Objects
BigInt Coercion with Objects
valueOf() returns a number, it can safely be coerced to bigint.Overriding ToPrimitive
You can override the whole defaultToPrimitive() operation by setting the special symbol property Symbol.toPrimitive:
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.
Same Types
If the types of both operands are the same,
== has the exact same behavior as ===. It immediately delegates to IsStrictlyEqual().Nullish Coercion
== is the most obvious place that JS exposes nullish coercive equality:
null nor undefined will ever be coercively equal to any other value in the language, other than to each other.
== Boolean Gotcha
Consider this snippet, assumingisLoggedIn is NOT holding a boolean value:
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":
if ("yes") passes, but if ("yes" == true) fails! Even worse:
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:Numbers
Even worse, recall how[] coerces to the string ""? By extension:
Coercion Absurdity
To prove the point, let’s take absurdity to level 11:[] == ![][] == false(! coerces to boolean, negates)"" == false(array to string)0 == false(empty string to number)0 == 0(boolean to number)0 === 0→true
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.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?”What TypeScript Provides
What TypeScript Provides
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.
What TypeScript Doesn't Solve
What TypeScript Doesn't Solve
- 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:: 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:
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:
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.
Known Facts
- If the types of operands for
==match, it behaves exactly the same as=== - If the types of operands for
===do NOT match, it will always returnfalse - If the types of operands for
==do NOT match, it will allow coercion (preferring numeric type-values) until types match
(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:
(1) Known Types
If you know the types ofx and y, there are two sub-conditions:
- (1a)
xandyare of the same type - (1b)
xandyare 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.
= would do nothing to make the code more clear. In fact, it makes it worse!
=== 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.
(1b) Known Mismatched Types
We need to comparex 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.
=== 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
Summarizing Type-Sensitive Equality Comparison
The Case for == Over ===
The case for always preferring
== over ===:- Whether you use TypeScript or not, the goal should be to have every part of code be type-aware
-
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
- When types match,
-
Only if you can’t know/predict the types, fall back to
===as a last resort (and probably add a comment explaining why)
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
== trueand== false - Embrace coercion responsibly

