Understanding Javascript Coercion in 5 Easy Steps

Greg Ashby
8 min readNov 28, 2017

--

I’ve recently found myself needing to get reacquainted with all the ‘quirks’ of Javascript. For anyone who uses Javascript only casually, trying to understand how it behaves when comparing different types of values can be daunting.

You’ve probably stumbled across some weird fact like:

[] == 0   // true
0 == '0' // true
[] == '0' // false!

which, shockingly, violates the principle of transitivity! I mean, if a == b and b == c, then a should also equal c, shouldn’t it?

Trying to understand what is happening, you’ll probably end up at a table that looks something like this:

typical javascript comparison table

and you’ll conclude the coercion rules are absolutely illogical, and proceed to avoid anything that might accidentally use them.

At least, that’s what I used to do. But I just finished reading the fantastic online book series https://github.com/getify/You-Dont-Know-JS, and surprisingly, I’d now argue that JS coercion behaviour is actually pretty easy to understand.

I’m going to attempt to describe how it works in a few, easy to learn steps, so you’ll never have to look at those awful equality tables ever again. At the end of this, you’ll even be able to easily understand why the result of parseInt(1/0, 19) is 18, and thus impress all your friends at your next dinner party!

Step 1: Understand Coercion ONLY Applies to Primitive Types

This may have been self-evident to you, but it wasn’t to me. JS is not ‘type safe’ after all, and it seems to be able to, or at least try to, coerce anything to anything. But it doesn’t. For any two objects:

anyObject == anyOtherObject

will always be false, unless they refer to the same actual object. It’s just a simple reference check. Coercion will ONLY occur when you attempt something that requires a value be forced into something else. anyObject + anyOtherObject for example will cause coercion to occur, but even then, both objects will first be converted to their primitive values.

To state this another way: in Javascript, objects NEVER get coerced. Coercion is the process of forcing one type of primitive value to another type of primitive value. If an object needs to ‘coerced’, Javascript will first attempt to get a primitive value for the object, and then coerce that primitive value if needed.

That might be playing with semantics a little bit, but thinking in terms of ‘coercion ONLY happens on primitive values’ greatly reduces the amount of ‘coercion rules’ you need to remember to understand how it works, since Javascript has only ̶f̶i̶v̶e̶ seven (see following note) primitive types: number, string, boolean, null, and undefined.

(NOTE: ES6 added the primitive type symbol but you don’t need to worry about anything coercing into a symbol, or vice versa, so we’re going to ignore that for coercion. Since the time of writing, biginthas also been added as another primitive, so its exact coercion details are not covered below and is ‘left as an exercise to the reader’. The general steps below are still the same however and should be useful to help you work out JS coercion behaviour).

Step 2: Understand How Objects “Coerce” to Primitives

The next question then is, how does Javascript get a primitive value for any object? And fortunately, this is pretty simple to understand. The process (known in the spec as the ToPrimitive operation) works like this:

// pseudo-codeif(obj.valueOf) {
pVal = obj.valueOf();
if(pVal is primitive) return pVal;
}
if(obj.toString) {
pVal = obj.toString();
if(pVal is primitive) return pVal;
}
// could not get a primitive value so
throw TypeError;

That’s it! It tries to get a primitive value from the valueOf function (if it exists). If that doesn’t work, it tries to get a primitive value from toString, which probably will exist and probably will return a string. (You can, of course, define a toString function that doesn’t return a string, but you are asking for trouble in doing so.) If neither of those functions produces a primitive value, then the object can NOT be coerced, so it just throws a TypeError.

NOTE: as of ES6 you can redefine this behaviour by providing a different implementation of the ToPrimitiveoperation like so: obj[Symbol.toPrimitive] = <insert your function here>. Your function should return a primitive or you’ll get a TypeError again (it won’t proceed to check for valueOf or toString).

Step 3: Know What toString Will Return

For most objects, toString simply returns '[object ObjectName]'. But you should also be aware of what (at least) a couple other common objects do:

Arrays:

  • toString returns a string that joins the string representations of its values with a comma. So[1, {}, 3].toString() will return '1,[object Object],3'
  • for empty arrays, or arrays with a single null value, it will return an empty string.

Functions:

  • toString will usually return some type of truncated text of the actual function code. For built-in functions, it will look like function funcName(){ [native code]}

Step 4: Know What Primitive Type Your Primitive Will Coerce To

So now that you’ve gotten a primitive value for your object (or you started with primitive value), you need to know what Javascript will attempt to coerce it to. And that depends on what you are trying to do.

First of all, if you are doing an operation like == and both values are the same primitive types, nothing happens. They can, obviously, already be compared to each other.

It’s really only the + operator where you have to think about it. And again the rules here are simple:

  • if your expression is +value it will always coerce to a number (this is known as a unary plus operation, and is a common idiom for forcing something to it’s number value)
  • if your expression is anything + string it will always coerce ‘anything’ to a string and concatenate them

That is, + is treated as an explicit coercion to a number if it’s used on a single value, a concatenation operation if it’s used on two values and either one is a string, otherwise it’s numeric addition.

For other operators:

  • if your expression requires a boolean (e.g. if(value) or !value) it will coerce to a boolean
  • for everything else, it will coerce the values to a number (including if you are using a boolean and a string… true — '2' will result in -1 and that will also make sense in a moment)

Step 5: Coercing Primitive Values

Now you know you’ve got a primitive of type X, and you know what type it will be coerced to. Finally, those primitive to primitive rules are pretty straight forward:

  • anything → boolean: If it’s null, undefined, 0 (or -0), '’ (empty string), or false, the result of the coercion is false. If it’s ANYTHING else, it’s true
  • number → string: the string representation of that number (which might be the scientific notation format… seeNumber.toString() if you need to know when it uses that).
  • string → number: if the string represents a valid number (including formats like scientific notation, hexadecimals, octals, etc), it’s converted to that number. And if it’s an empty string, it’s 0. Otherwise it’s just NaN.
  • boolean → number: true becomes 1, false becomes 0
  • null or undefined → number: 0 and NaN respectively
  • null or undefined → string: 'null' and 'undefined' respectively

That’s a pretty simple list of rules to learn, no? Especially since, for the most part, they make perfect sense. Basically, it’s just:

  1. A string that doesn’t represent a number is NaN.
  2. There’s only 5 things that coerce to false, everything else just becomes true.
  3. Null, undefined, and numbers are all spelled exactly like the strings that you’d expect.
  4. undefined is Not-A-Number.

Probably the only thing you could say is a surprise is that null is 0 instead of NaN. So just memorize that, and the 5 falsy values, and everything else will probably be coerced into exactly what you’d expect it to become.

Five Steps, and Some Practise

Remember that table at the top you thought you had to memorize? Go back through it now and try some combinations with the steps above to see if the results really do makes sense. They should, and if they don’t, I’ve probably mis-stated how it works, so please let me know in the comments and I’ll clarify it!

For even more practice (and more detailed explanations of the coercion steps), go through the “edge cases” in https://github.com/getify/You-Dont-Know-JS/blob/master/types%20%26%20grammar/ch4.md#edge-cases

While the book describes those as “edge cases”, I’d actually argue even those cases make perfect sense when you understand and walk through the 5 steps above.

One Last Gotcha

The only case I’ve seen thus far which still seems puzzling at first is this:

[] + {}; // "[object Object]"
{} + []; // 0

That seems to break transitivity again, but in reality, it just takes understanding one more thing about Javascript: how it treats {} depending on the context.

In the first example, {} is interpreted as an empty object, so using the rules above you get '’ + ‘[object Object]'

In the second case {} is interpreted as an empty code block, which does nothing. So that expression is actually just +[] which now you know just coerces to 0.

Knowing that, you can probably also understand why {} + ‘hello' results in NaNnow, can’t you? :D

(If you can’t, walk through the steps again until you do. Once that clicks, you’ll never have to memorize another coercion rules table, or be afraid of relying on coercion).

Appendix: Really, really, understanding ToPrimitive

I said in the opening that this would even help you understand why parseInt(1/0, 19) equals 18. Let’s walk through that, as it uncovers another key detail to understand that I didn’t mention earlier.

First of all, parseInt needs a string to parse. So when it attempts to do the ToPrimitive operation described above, it actually passes in a hint saying it wants a string. In most cases, that hint will be number but in some contexts, like needing a string to parse, it will be string.

(In fact, exploring this is probably the best way to really learn JS coercion. Try creating some blank objects (var obj = Object.create(null);), adding valueOf, toString, and Symbol.toPrimitive functions that log what’s happening (including the ‘hint’), and then experiment with different operations until you understand what’s getting invoked when, with what hints, and how to get a TypeError).

Back to parseInt… knowing it needs to get a primitive string value of the first argument, let’s breakdown how we get the final result:

  1. 1/0 results in a special Javascript number called Infinity . So that’s the first argument’s actual value
  2. with the string hint, Infinity becomes the string 'Infinity'
  3. GOTCHA: unlike coercion, where any non-number string becomes NaN, parseInt actually attempts to parse the string in a tolerant way… that is, one character at a time, until it encounters an invalid character. So that’s another thing to keep in mind if you’re confused about what JS is doing — is it actually ‘coercing’ or is it just doing something else (like parsing)?
  4. The second argument is the ‘radix’, and in ‘base-19’, I is equal to 18 (of course!)
  5. In base-19, n (the second character of 'Infinity') is not a valid ‘digit’, so the function stops there, and just returns 18.

I told you that would make perfect sense ;)

Now go forth and amuse your dinner guests with this incredible knowledge!

--

--