You Don't Know JS Yet: Types & Grammar - 2nd Edition
Chapter 4: Coercing Values
NOTE: |
---|
Work in progress |
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. Here's why: the line between explicit and implicit is not clear and objective, it's fairly subjective. If you think a type conversion is implicit (and thus coercion), but I think it's explicit (and thus not a coercion), the distinction becomes irrelevant.
Keep that subjectivity in mind as we explore various explicit and implicit forms of coercion. In fact, here's a spoiler: most of the coercions could be argued as either, so we'll be looking at them with such balanced perspective.
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.
But that feeling is not new. 14+ years ago, Douglas Crockford's book "The Good Parts" also famously decried implicit coercion as one of the bad parts. Even Brendan Eich, creator of JS, regularly claims that implicit coercion was a mistake1 in the early design of the language that he now regrets.
If you've been around JS for more than a few months, you've almost certainly heard these opinions voiced strongly and predominantly. And if you've been around JS for years or more, you probably have your mind already made up.
In fact, I think you'd be hard pressed to name hardly 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.
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. Hmmmm...
Douglas Crockford says to avoid the mistake of implicit coercion2, but his code uses if (..)
statements with non-boolean values evaluated. 3 Many have dismissed my pointing that out in the past, with the claim that conversion-to-boolean isn't really coercion. Ummm... ok?
Brendan Eich says he regrets implicit coercion, but yet he openly endorses4 idioms like x + ""
(and others!) to coerce the value in x
to a string (we'll cover this later); and that's most definitely an implicit coercion.
So what do we make of this dissonance? Is it merely a, "do as I say, not as I do" minor self-contradiction? Or is there more to it?
I am not going to pass a final judgement here yet, but I want you the reader to deeply ponder that question, as you continue throughout this chapter and book.
Abstracts
Now that I've challenged you to examine coercion in more depth than you may have ever previously indulged, let's first look at the foundations of how coercion occurs, according to the JS specification.
The specification details a number of abstract operations5 that dictate internal conversion from one value-type to another. It's important to be aware of these operations, as coercive mechanics in the language mix and match them in various ways.
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. Instead, we activate them implicitly/indirectly depending on the statements/expressions 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, such as whether a string is empty or has anything in it.
When non-boolean values are encountered in a context that requires a boolean -- such as the condition clause of an if
statement or for
loop -- the ToBoolean(..)
6 abstract operation is activated to facilitate the coercion.
All values in JS are in one of two buckets: truthy or falsy. Truthy values coerce via the ToBoolean()
operation to true
, whereas falsy values coerce to false
:
// ToBoolean() is abstract
ToBoolean(undefined); // false
ToBoolean(null); // false
ToBoolean(""); // false
ToBoolean(0); // false
ToBoolean(-0); // false
ToBoolean(0n); // false
ToBoolean(NaN); // false
Simple rule: any other value that's not in the above list is truthy and coerces via ToBoolean()
to true
:
ToBoolean("hello"); // true
ToBoolean(42); // true
ToBoolean([ 1, 2, 3 ]); // true
ToBoolean({ a: 1 }); // true
Even values like " "
(string with only whitespace), []
(empty array), and {}
(empty object), which may seem intuitively like they're more "false" than "true", nevertheless coerce to true
.
WARNING: |
---|
There are narrow, tricky exceptions to this truthy rule. For example, the web platform has deprecated the long-standing document.all collection/array feature, though it cannot be removed entirely -- that would break too many sites. Even where document.all is still defined, it behaves as a "falsy object"7 -- undefined which then coerces to false ; this means legacy conditional checks like if (document.all) { .. } no longer pass. |
The ToBoolean()
coercion operation is basically a lookup table rather than an algorithm of steps to use in coercions a non-boolean to a boolean. Thus, some developers assert that this isn't really coercion the way other abstract coercion operations are. I think that's bogus. ToBoolean()
converts from non-boolean value-types to a boolean, and that's clear cut type coercion (even if it's a very simple lookup instead of an algorithm).
Keep in mind: these rules of boolean coercion only apply when ToBoolean()
is actually activated. There are constructs/idioms in the JS language that may appear to involve boolean coercion but which don't actually do so. More on these later.
ToPrimitive
Any value that's not already a primitive can be reduced to a primitive using the ToPrimitive()
(specifically, OrdinaryToPrimitive()
8) abstract operation. Generally, the ToPrimitive()
is given a hint to tell it whether a number
or string
is preferred.
// ToPrimitive() is abstract
ToPrimitive({ a: 1 },"string"); // "[object Object]"
ToPrimitive({ a: 1 },"number"); // NaN
The ToPrimitive()
operation will look on the object provided, for either a toString()
method or a valueOf()
method; the order it looks for those is controlled by the hint. "string"
means check in toString()
/ valueOf()
order, whereas "number"
(or no hint) means check in valueOf()
/ toString()
order.
If the method returns a value matching the hinted type, the operation is finished. But if the method doesn't return a value of the hinted type, ToPrimitive()
will then look for and invoke the other method (if found).
If the attempts at method invocation fail to produce a value of the hinted type, the final return value is forcibly coerced via the corresponding abstract operation: ToString()
or ToNumber()
.
ToString
Pretty much any value that's not already a string can be coerced to a string representation, via ToString()
. 9 This is usually quite intuitive, especially with primitive values:
// ToString() is abstract
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"
There are some results that may vary from common intuition. As mentioned in Chapter 2, very large or very small numbers will be represented using scientific notation:
ToString(Number.MAX_VALUE); // "1.7976931348623157e+308"
ToString(Math.EPSILON); // "2.220446049250313e-16"
Another counter-intuitive result comes from -0
:
ToString(-0); // "0" -- wtf?
This isn't a bug, it's just an intentional behavior from the earliest days of JS, based on the assumption that developers generally wouldn't want to ever see a negative-zero output.
One primitive value-type that is not allowed to be coerced (implicitly, at least) to string is symbol
:
ToString(Symbol("ok")); // TypeError exception thrown
WARNING: |
---|
Calling the String() 10 concrete function (without new operator) is generally thought of as merely invoking the ToString() abstract operation. While that's mostly true, it's not entirely so. String(Symbol("ok")) works, whereas the abstract ToString(Symbol(..)) itself throws an exception. More on String(..) later in this chapter. |