Implicit Coercion

Over the last two blog posts I've taken a deep look into the ToString, ToNumber, ToBoolean, and ToPrimitive Abstract Operations that govern type conversion as well as different methods of performing Explicit Coercion in JavaScript. It's time to continue this little series with an exploration of Implicit Coercion, which is a touchy subject. A lot of the discussion around JavaScript's shortcomings as a programming language seem rooted in frustrating experiences with Implicit Coercion. Once again, I don't intend to talk about those opinions, and instead I simply want to understand Implicit Coercion as a concept, and how it fits in with Explicit Coercion and the Abstract Operations that make type coercion work the way that it does in JavaScript.

To review one last time, Coercion is the operation of converting a value from one type to another. In JavaScript, coercion always results in either a String, a Number, or a Boolean.

The entire topic of coercion is all about three specific operations:

  1. Converting non-String values to String values
  2. Converting non-Number values to Number values
  3. Converting non-Boolean values to Boolean values

The underlying mechanism that makes coercion work in JavaScript is a set of Abstract Operations, specifically ToString, which handles the conversion of a non-String primitive to a String representation, ToNumber, which handles the conversion of a non-Number primitive to a Number representation, ToPrimitive, which handles the conversion of a complex value (like an Object) to a primitive representation (so that either ToString or ToNumber can further convert them), and ToBoolean, which defines which value types are "falsy" (i.e., will coerce to false) or "truthy" (i.e., will coerce to true).

There are a number of ways that coercion can occur that are generally considered clear and straightforward, and these methods are referred to as Explicit Coercion. The global String() function and the .toString() method will convert non-String values to String representations. The global Number() function, the parseInt() and parseFloat() functions, and + and - unary operators will convert non-Number values to Number representations. The global Boolean() function and the ! unary operator will convert non-Boolean values to Boolean representations.

What is Implicit Coercion?

Implicit Coercion is a coercion operation where the coercion seems to occur as a side-effect of some other operation, like a mathematical operation or string concatenation. In these cases, the type coercion often happens "in the background", so to speak, so it may not be as clear as to what value type is being converted to another value type and why.

Implicitly Coercing to a String

Aside from numeric addition operations, the + operator can be used to concatenate Strings in JavaScript, like so:

var str1 = "hey there ";
var str2 = "what's up";

var result = str1 + str2;

console.log(result);
// expected result: "hey there what's up"

The + operator's concatenation operation produces a String. In this case, it's simply combining the two Strings that are given as its operands. In this example, this is not coercion at all, just the combination of the two operand String values into one.

However, when the + operator produces a String value, but at least one of its operands is a non-String, then a coercion operation must have occurred, converting that non-String operand into a String representation that could then be concatenated. This is how Implicit Coercion comes into play, since the type conversion happens as a side effect of another operation, namely the concatenation operation.

Let's look at this in action:

var str = "hello";
var num = 420;

var result = str + num;

console.log(result);
// expected result: "hello420"

In this case we use the + operator with a String "hello" as one of its operands and the Number 420 as the other operand. Because one of the operands is a String, the + operator will go for a concatenation operation, producing a String. But before it can do that, it needs to coerce the non-String operand to a String representation using the rules laid out in the ToString Abstract Operation. In this case, because 420 is a Number, ToString simply returns a natural String representation of that Number, "420". Now that the two operands are String values, the concatenation can occur, producing the result, the String "hello420".

Because it's a result of the concatenation operation, the coercion of the non-String operand to a String representation is said to be an Implicit Coercion. On top of that, the use of the + operator in this context may be confusing to a developer who is not as familiar with these rules, and expects + to be strictly an operator used for addition.

Consider another, similar example:

var str = "";
var num = 420;

var result = str + num;

console.log(result);
// expected result: "420"

console.log(typeof(result));
// expected result: "string"

In this example we are concatenating an empty String "" with the Number 420. 420 gets coerced to a String representation as in the previous example, and when "420" gets concatenated with an empty String, the result is just the String "420". This pattern of the + operator having an empty String "" as its operand to coerce the other, non-String, operand to a String representation is very, very common, and it's all based on Implicit Coercion. It's worth familiarizing yourself with the pattern, so you know what the developer is trying to do when you run across it in the wild!

It seems straightforward enough that when one of +'s operands is a String, then + will do a concatenation operation, and thereby do any necessary coercion of a non-String value to a String representation. However, it's not as simple as that. Let's look at another example:

var arr = [4, 2, 0];
var num = 100;

var result = arr + num;

console.log(result);
// expected result: 4,2,0100

console.log(typeof(result));
// expected result: "string"

In this example we've given to the + operator an Array operand, arr, with the value [4, 2, 0], and a Number operand, num, with the value 100. The result of the operation is the String "4,2,0100". What happened here? Unfortunately, the answer is complicated.

When the + operator receives a complex value, like an Object or an Array, for either of its operands, it abides by the ToPrimitive Abstract Operation for that value, hoping to produce a simple primitive value that it can then do further work on, either as a Number that it can do math with or a String that it can concatenate. This Abstract Operation looks for a [Symbol.toPrimitive] method, which isn't found here, then for the result of the .valueOf() method, which in this case just produces the array itself (not a primitive, so not something that the + operator can use), and then finally falls back to the .toString() method, which as we now know produces the String "4,2,0".

Since this complex value produced a String, the + operator knows it needs to concatenate, so it coerces 100 to its natural String representation, "100", and then concatenates it with "4,2,0", producing the result we see here: "4,2,0100".

Implicitly coercing to a String value all centers around the + operator, and if one of its operands is a String or its ToPrimitive algorithm produces a String, then concatenation will occur, and any non-String operand values will get coerced to their String representations in order to allow for that concatenation to happen.

With this understanding in mind, let's look now at how the different types implicitly coerce to String representations. I will be concatenating with an empty String "" to make everything as clear as I can.

Concatenating null with an empty String produces "null":

null + "";
// expected result: "null"

Concatenating undefined with an empty String produces "undefined":

undefined + "";
// expected result: "undefined"

Concatenating a Boolean with an empty String produces the String representation of that Boolean:

true + "";
// expected result: "true"

false + "";
// expected result: "false"

An Array with no user-defined .toString() method will use the generic one that comes from Array.prototype.toString(), which internally calls the .join() method:

var arr = ["Joey", "Reyes", "rules"];
arr + "";
// expected result: "Joey,Reyes,rules"

If both operands are Arrays, then this same coercion using Array.prototype.toString() will happen for each Array separately, and their results will get concatenated:

var arr1 = ["Joey", "Reyes", "rules"];
var arr2 = ["JavaScript", "drools"];
arr1 + arr2;
// expected result: "Joey,Reyes,rulesJavaScript,drools"

Function.prototype also has its own .toString() method, which generally returns a String representing the source code of the function. Thus, when you concatenate a Function and an empty String you get something like:

function doSomething(a) {
  return a * a;
}

doSomething + "";
// expected result: "function doSomething(a) {\n  return a * a;\n}"

However, where Symbol.prototype also has a .toString() method, you are not permitted to use a string concatenation operation to implicitly coerce the Symbol to a String representation:

var sym = Symbol('howdy');

sym + "";
// expected result: Uncaught TypeError

Objects are a bit more complicated. As discussed previously, an Object will coerce to a String representation following the algorithm defined in the ToPrimitive Abstract Operation. First it looks for a [Symbol.toPrimitive] method on the Object in question. If [Symbol.toPrimitive] is found, and it returns a String, then that String is what + will use to concatenate.

var obj = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
  [Symbol.toPrimitive](hint) {
    return this.b;
  },
};

var str = "";

var result = obj + str;
console.log(result);
// expected result: "Joey Reyes rules"

If [Symbol.toPrimitive] is not an available method, then ToPrimitive will look for a .valueOf() method:

var obj = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
  valueOf() {
    return this.b;
  },
};

var str = "";

var result = obj + str;
console.log(result);
// expected result: "Joey Reyes rules"

If a user-defined .valueOf() method is not available, then ToPrimitive will call Object.prototype.valueOf(). However, this just produces the contents of the Object itself:

var obj = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
};

obj.valueOf();
// expected result: {a: 100, b: 'Joey Reyes rules', c: true, d: Array(3)}

Because we need a Primitive and not a complex value, ToPrimitive then proceeds to look for a .toString() method:

var obj = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
  toString() {
    return this.b;
  },
};

var str = "";

var result = obj + str;
console.log(result);
// expected result: "Joey Reyes rules"

If a defined .toString() method is not found, it uses the generic .toString() method from Object.prototype.toString(), which returns the [[Class]] property, ultimately outputting the String "[object Object]":

var obj = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
};

var str = "";

var result = obj + str;
console.log(result);
// expected result: "[object Object]"

If all of this sounds familiar, it's because this is now the third blog post in a row where I've written out this same exact logic.

In most cases using the [non-string value] + "" pattern to coerce [non-string value] to its String representation counterpart works the same as passing [non-string value] to the global String() function. However, you should be aware of one crucial difference: If the Object in question doesn't have a [Symbol.toPrimitive] method, then [non-string value] + "" will call the .valueOf() method before calling the .toString() method. The global String() function on the other hand calls the .toString() method first. In most cases the result will be the same, but if you have written an Object and defined your own .valueOf() and .toString() methods on that Object, then you should be aware which method these two coercion approaches will take.

var obj = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
  e: "JavaScript drools",
  toString() {
    return this.b;
  },
  valueOf() {
    return this.e;
  },
};

var str = "";

obj + str;
// expected result: "JavaScript drools"

String(obj);
// expected result: "Joey Reyes rules"

At the end of the day, using the + operator where one of the operands is a String or results in a String from the ToPrimitive abstract operation will result in a String. If one of +'s operands is a non-String value and the other is an empty String, it's effectively the same as passing that non-String value to the String() global function (though there is some nuance to be aware of).

Implicitly Coercing to a Number

Let's take a bit of a side quest now to examine some other mathematical operators, specifically -, *, /, and %.

- is commonly used for subtraction:

var num1 = 100;
var num2 = 10;

var result = num1 - num2;

console.log(result);
// expected result: 90

* is commonly used for multiplication:

var num1 = 100;
var num2 = 10;

var result = num1 * num2;

console.log(result);
// expected result: 1000

/ is commonly used for division:

var num1 = 100;
var num2 = 10;

var result = num1 / num2;

console.log(result);
// expected result: 10

% is commonly used to calculate the division remainder:

var num1 = 100;
var num2 = 10;

var result = num1 % num2;

console.log(result);
// expected result 0

If either of the operands for any of these operators is a non-Number value, then the operator will attempt to convert that value to a Number representation using the ToNumber Abstract Operation.

null coerces to 0:

null - 100;
// expected result: -100

undefined coerces to NaN:

undefined - 100;
// expected result: NaN

true coerces to 1 and false coerces to 0:

true - 100;
// expected result: -99

false - 100;
// expected result: -100

Strings will coerce to their Number equivalents if possible...

"420" - 100;
// expected result: 320

...otherwise they will coerce to NaN:

"four hundred twenty" - 100;
// expected result: NaN

An Object will first look for and utilize the [Symbol.toPrimitive] method with a hint of number:

var obj = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
  [Symbol.toPrimitive](hint) {
    console.log(`hint: ${hint}`); // let's see what the hint is
    return hint === "string" ? `{ b: "${this.b}" }` : this.a;
  },
};

var num = 100;

var result = obj - num;
console.log(result);
// expected result:
// hint: number
// 0

If [Symbol.toPrimitive] isn't available it will look for a .valueOf() method:

var obj = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
  valueOf() {
    return 420;
  },
};

var num = 100;

var result = obj - num;
console.log(result);
// expected result: 320

If a user-defined .valueOf() method is not available, then ToPrimitive will call Object.prototype.valueOf(). This just produces the contents of the Object itself:

var obj = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
};

obj.valueOf();
// expected result: {a: 100, b: 'Joey Reyes rules', c: true, d: Array(3)}

Because we need a Primitive and not a complex value, ToPrimitive then proceeds to look for a .toString() method:

var obj = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
  toString() {
    return this.b;
  },
};

var num = 100;

var result = obj - num;
console.log(result);
// expected result: NaN

If a defined .toString() method is not found, it uses the generic .toString() method from Object.prototype.toString(), which returns the [[Class]] property, ultimately returning the String "[object Object]". Because this String is passed to -, *, /, or %, the result is NaN:

var obj = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
};

var num = 100;

var result = obj - num;
console.log(result);
// expected result: NaN

For an Array, the generic Array.prototype.toString() method internally calls the .join() method, returning the contents of the Array separated by commas. If the Array only has a single element that is either a Number or a value that can be coerced to a Number, then we can actually get a meaningful mathematical operation:

var arr = ["420"];

var num = 100;

var result = arr - num;
console.log(result);
// expected result: 320

However, in all other cases you're likely just going to wind up with NaN:

var arr = ["420", "1000", {valueOf() { return 2000; }}];

var num = 100;

var result = arr - num;
console.log(result);
// expected result: NaN

Because Function.prototype.valueOf() just returns the Function itself, you get NaN when one of the -/*///%'s operands is a Function:

function doSomething(a) {
  return a * a;
}

var num = 100;

var result = doSomething - num;
console.log(result);
// expected result: NaN

You can, of course, call the Function, and if that returns a Number then you're good (although that isn't really coercion, is it?):

function doSomething(a) {
  return a * a;
}

var num = 100;

var result = doSomething(100) - num;
console.log(result);
// expected result: 9900

As with implicit String coercion, we are not allowed to implicitly coerce a Symbol to a Number:

var sym = Symbol('howdy');

sym - 100;
// expected result: Uncaught TypeError

We've focused on the -, *, /, and % operators for implicit Number coercion so far, but that's not the whole picture. Let's turn back to the + operator, which we know will perform concatenation if either of its operands is a String or coerces to a String as a result of the ToPrimitive Abstract Operation. If, on the other hand, the operand is a non-String primitive that can cleanly coerce to a Number, then it will do so.

null coerces to 0:

null + 100;
// expected result: 100

undefined coerces to NaN (and this operation will not coerce to a String):

undefined + 100;
// expected result: NaN

true coerces to 1 and false coerces to 0:

true + 100;
// expected result: 101

false + 100;
// expected result: 100

And lastly, any Object that coerces to a Number from a [Symbol.toPrimitive], .valueOf(), or (for some reason) .toString() method will complete the addition operation:

var obj = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
  valueOf() {
    return this.a;
  },
};

var num = 100;

var result = obj + num;
console.log(result);
// expected result: 200

Implicitly Coercing to a Boolean

We are now left with implicit coercion of non-Boolean values to Boolean representations, which happen in 7 specific operations:

  1. The test condition of an if statement. For example,
if (condition) { /* do work here */ }
  1. The test condition (second expression) within the parenthesis of a for loop. For example,
for (let counter = 0; counter <= 10; counter += 1) { /* do work here */ }
  1. The test condition of a while loop. For example,
while (counter <= 420) { /* do work here */}
  1. The test condition of a do...while loop. For example:
do {
  /* some work here, always runs at least once */
} while (counter <= 420);
  1. The test condition (first expression) of a ternary expression. For example:
condition ? "condition has been met" : "condition has not been met";
  1. The left hand operand in the Logical Or || operator. For example:
(condition1 || condition2);
  1. The left hand operand in the Logical And && operator. For example:
(condition1 && condition2);

Any non-Boolean values used within these contexts will be implicitly coerced to a Boolean in order to complete the necessary conditional or logical operation. As you may be able to predict by now, this coercion happens in accordance with the ToBoolean Abstract Operation, which defines which values types are "falsy" (i.e., will coerce to false) and which value types are "truthy" (i.e., will coerce to true).

To review, the "falsy" values are undefined, null, +0, -0, NaN and "" (empty String). The "truthy" values are all other values and types.

Let's compare and contrast some of "falsy" and "truthy" values within some of the contexts above.

How about an undefined value ("falsy") and an empty Array ("truthy") within an if () statement:

var undefinedThing;
var definedThing = [];

if (undefinedThing) {
  console.log("I'm never going to run");
}
// expected result: this condition never passes

if (definedThing) {
  console.log("hey there, from inside of the if statement");
}
// expected result: "hey there, from inside of the if statement"

Or a null value ("falsy") and an Object ("truthy") within a while Loop:

var nullThing = null;
var obj = {
  counter: 0,
};

while (nullThing) {
  console.log("I'm never going to run");
}
// expected result: this condition never passes

while (obj.counter < 10) {
  console.log("counting up!");
  obj.counter += 1;
}
// expected result: ten instances of "counting up!" and obj.counter counts up

Or -0 ("falsy") and another Number, like 420, within a do...while loop:

var neg0 = -0;
var num = 420;

do {
  console.log("I always run at least once");
} while (neg0);
// expected result: one instance of "I always run at least once", and then nothing else

do {
  console.log("counting down!");
  num -= 1;
} while (num >= 400);
// expected result: 21 instances of "counting down!" and num counts down

Or an empty String ("falsy") and a non-empty String ("truthy") within a ternary expression:

var emptyStr = "";
var str = "Joey Reyes rules";

emptyStr ? "String is not empty" : "String is empty";
// expected result: "String is empty"

str ? "String is not empty" : "String is empty";
// expected result: "String is not empty"

The Logical Or || and Logical And && operators work a little differently, and perhaps not as one would expect. They each accept two operands, a left hand operand and a right hand operand. If the left hand operand is not already a Boolean, then the operator will do a ToBoolean operation to coerce the value to a Boolean representation.

What happens next depends on the operator.

If the left hand operand coercion results in true (or the operand is a Boolean true to begin with), then the Logical Or || operator will return the initial value of the left hand operand:

"hello" || 420;
// expected result: "hello"

If the left hand operand coercion results in false (or the operand is a Boolean false to begin with), then the Logical Or || operator will return the initial value of the right hand operand:

"" || 420;
// expected result: 420

If the left hand operand coercion results in true (or the operand is a Boolean true to begin with), then the Logical And && operator will return the initial value of the right hand operand:

"hello" && 420;
// expected result: 420

This is the case, even if the right hand operand is a falsy value (or a Boolean false):

"hello" && null;
// expected result: null

If the left hand operand coercion results in false (or the operand is a Boolean false to begin with), then the Logical And && operator will return the initial value of the left hand operand:

"" && 420;
// expected result: ""

This may be a bit unexpected, as logical operators in other languages simply return a true or false Boolean in all cases. Here we only get a Boolean value returned if the initial operand value is itself a Boolean and the proper logical algorithm returns that Boolean.

It's a bit confusing. But armed with this understanding, we can now see how for something like:

if ("" || 420) {
  console.log("Condition met. Joey Reyes rules.");
}
// expected result: "Condition met. Joey Reyes rules."

What's actually happening are two implicit ToBoolean coercions. First, to determine which of the two operand values given to the Logical Or || operator should get returned, the left hand operand, "" gets coerced to a Boolean according to the ToBoolean Abstract Operation. Because ""'s Boolean representation is false, || returns the right hand operand, the Number 420.

From there the conditional statement's test condition is basically:

if (420) {
  console.log("Condition met. Joey Reyes rules.");
}

The if () test condition then implicitly coerces the Number 420, also according to the ToBoolean Abstract Operation. Because 420's Boolean representation is true, the if () test condition is met, so the code block runs.

Summary

Implicit Coercion is a coercion operation where the coercion seems to occur as a side-effect of some other operation, like a mathematical operation or string concatenation.

You can implicitly coerce non-String values to String representations as a result of the + operator's concatenation operation. This happens when at least one of +'s operands is either a String or a value whose ToPrimitive Abstract Operation produces a String. A very common pattern for coercing non-String values into String representations is [non-string value] + "". For the most part this produces the same results as the explicit String([non-string value]) pattern, but not in all cases.

You can, however, also implicitly coerce non-Number, non-String values to Number representations using the + operator, as long as that value is a primitive that cleanly coerces to a Number, or is a complex value that coerces to a Number as a result of the ToPrimitive Abstract Operation algorithm. Otherwise, the other mathematical operators (-, *, /, %) will always try to coerce non-Number operands to Number representations in order to complete the mathematical operation. If the non-Number operand can't be coerced to a Number, then the mathematical operation will return NaN (unless the operand is a Symbol, in which case a TypeError is thrown).

Finally, there are seven operations where non-Boolean values get implicitly coerced to Boolean representations:

  1. The test condition of an if statement
  2. The test condition (second expression) within the parenthesis of a for loop
  3. The test condition of a while loop
  4. The test condition of a do...while loop
  5. The test condition (first expression) of a ternary expression
  6. The left hand operand in the Logical Or || operator
  7. The left hand operand in the Logical And && operator

It's worth noting that the Logical Or and Logical And operators may not return the value that you would normally think they should, and that when used in conjunction with one of the other implicit Boolean-forcing operations, can actually make more than one implicit Boolean coercion happen.

Glossary

  • Coercion: The operation of converting a value from one type to another. In JavaScript, coercion always results in a scalar primitive value, namely a String, a Number, or a Boolean.
  • Explicit Coercion: A coercion operation where it's very clear from the code what value type is getting converted to another value type and why.
  • Implicit Coercion: A coercion operation where the coercion seems to occur as a side-effect of some other operation (like a mathematical operation), and thereby it's less clear what value type is converted to another value type and why.
  • Scalar Primitive Value:
    • Primitive refers to a data type that is a basic building block of other data types, not one composed of other data types. The six primitive data types in JavaScript are undefined, Boolean, Number, String, BigInt, Symbol, and null.
    • Scalar refers to a data type that has a single value or unit of data. The scalar data types in JavaScript are Number, Boolean, and String. Contrast this with null or undefined, whose label is both its type and its value.
    • A third category would be a Complex, Compound, or Structural value, such as an Object (and thereby also Array) or a Function. These types are made up of other types.
  • Abstract Operations: Descriptions of various semantics within JavaScript that help establish norms for operations available across disparate JavaScript implementations.
    • ToString: The Abstract Operation that handles conversions of non-String values to String representations.
    • ToNumber: The Abstract Operation that handles conversions of non-Number values to Number representations.
    • ToPrimitive: The Abstract Operation that handles conversions of complex values to primitive values.
    • ToBoolean: The Abstract Operation that handles conversions of non-Boolean values to Boolean representations.

Sources / Further Reading