Coercion: ToString, ToNumber, and ToBoolean

Within the JavaScript community, the term "coercion" is widely used to describe the operation of converting a value from one type to another. It's a touchy subject, and one that causes a great deal of confusion when a coercion operation produces a result that a developer isn't expecting. I likely won't be writing about the prevailing opinions and feelings about coercion in JavaScript on this blog. That controversy is well-discussed across dozens of books, hundreds of blogs, and countless StackOverflow discussions. I have nothing new to add to the debate, and mostly take the pragmatic stance that coercion is a part of the language for good, and it's a worthwhile exercise in understanding it, quirks and all.

Over the next few blog posts I plan to take a deep dive into coercion: how it works, what value types coerce to what other value types, and how these operations actually take place (both explicitly and implicitly). My hope is to whittle down the topic into something digestible, understandable, and referenceable, so that hopefully I (and any developers reading this) won't be so baffled when coming across some strange coercion in future programming.

What is Coercion?

Coercion is the operation of converting a value from one type to another. In JavaScript, coercion always results in a scalar primitive value (see Glossary below), namely 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

That's it! These conversions can happen either as Explicit Coercion, where it's very clear from the code what value type is getting converted to another value type and why, or as Implicit Coercion, where the coercion seems to occur as a side-effect of some other operation, and thereby it's less clear what value type is getting converted to another value type and why.

I will be diving into Explicit Coercion and Implicit Coercion in later posts on this blog, but before going into those topics, it's necessary to understand a few Abstract Operations, namely ToString, ToNumber, and ToBoolean, as these Abstract Operations govern how we convert different value types to String, Number, or Boolean value types. A fourth Abstract Operation, ToPrimitive, also plays a role, so we will be digging into that one along the way. Let's go.

Abstract Operations

Abstract Operations are a massive topic of their own right. Essentially, Abstract Operations are descriptions of various semantics within JavaScript that help establish norms for operations available across disparate JavaScript implementations. You want the code you write to be predictable and compatible whether it runs within Google Chrome, Node, Mozilla Firefox, or some other environment built by some other vendor, right? Abstract Operations dictate how things like comparison operators, getters and setters on Objects, or iterators on Arrays or Strings should work.

Oddly enough, the operations themselves aren't a part of the ECMAScript language, but their definitions are, and there are a ton of them, including 22 different Type Conversion Abstract Operations. You can read about all of them if you really want to, but since coercion is only about converting different value types to String, Number, or Boolean value types, we only need to focus on ToString, ToNumber, ToPrimitive, and ToBoolean for a really good foundation for further understanding coercion.

ToString Abstract Operation

When any non-String value is coerced to a String value, the conversion is handled by the ToString Abstract Operation, which is defined in Section 7.1.17 of the ECMAScript Language Specification.

Per the specification, built-in primitive values have natural string representations that they coerce to:

null coerces to "null":

String(null);
// expected result: "null"

undefined coerces to "undefined":

String(undefined);
// expected result: "undefined"

true coerces to "true":

String(true);
// expected result: "true"

false coerces to "false":

String(false);
// expected result: "false"

Numbers also generally convert naturally...

String(420);
// expected result: "420"

String(-100);
// expected result: "-100"

...but very large and very small Numbers are represented exponentially:

String(1230000000000000000000);
// expected result: "1.23e+21"

String(.00000000000000123);
// expected result: "1.23e-15"

...and -0 will coerce to a positive 0 representation:

String(-0);
// expected result: "0"

Objects are a little bit more complicated, since they can be composed of several different data types. Take the following Object as an example:

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

This Object, obj, has four properties:

  • a, which is the Number value 100
  • b, which is the String value "Joey Reyes rules"
  • c, which is the boolean true
  • d, which is an Array of the Numbers 1, 2, and 3.

What happens if I run String(obj)?

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

String(obj);
// expected result: "[object Object]"

The output is "[object Object]". That isn't very helpful. To understand this output and be able to write code that is predictable and usable, we need to take a little detour into another Abstract Operation, called ToPrimitive.

ToPrimitive Abstract Operation

When an Object (and thereby an Array) gets coerced to a String or Boolean value, that Object first goes through a different Abstract Operation, called ToPrimitive. ToPrimitive takes a complex value type (like an Object) and determines how to return it as a primitive (like a String or a Number).

To do this, ToPrimitive checks for a few things.

First, it checks whether the Object in question has a [Symbol.toPrimitive] method defined. Introduced in ES6, [Symbol.toPrimitive] is a way that we can define predictable type conversion behavior when the Object in question is used as a String, as a Number, or as something else.

Let's add onto our example obj from before:

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;
  },
};

We have the same properties as before, but have now added a [Symbol.toPrimitive] method. This Symbol has a built-in hint parameter, which is basically the context that the conversion operation gets called in. To help clarify what's going on here, I will first log the hint out to the console.

If that hint is string, we know that obj is getting used within a String context, and can define what value to return. In this case I'm just going to return the b property wrapped in some curly braces.

To test this out, let's run the following:

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;
  },
};

String(obj);
/*
 * expected output:
 * hint: string
 * '{ b: "Joey Reyes rules" }'
 */

After running String(obj), we no longer get "[object Object]", but now we get a helpful notice that hint is string, and then we get '{ b: "Joey Reyes rules" }', which is what we specified to return when hint is string. In this way we can define what value to return when coercing an Object to a String.

.toString() Fallback

So what happens if there's no [Symbol.toPrimitive] method on the Object, or we're working in a pre-ES6 environment that doesn't recognize [Symbol.toPrimitive]? The old school way of converting a JavaScript Object to a String value as defined in the ToPrimitive Abstract Operation is to look for a .toString() method.

By default, all Objects have a generic .toString() method they inherit from Object.prototype.toString(). If no other .toString() method is found on the Object, then this generic one will get called, which returns the internal [[Class]] property, resulting in a frequently seen (but seldom helpful) "[object Object]":

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

// Explicitly coercing `obj` to a string:
String(obj);
// expected result: "[object Object]"

// Calling the `.toString()` method:
obj.toString();
// expected result: "[object Object]"

This explains the behavior we saw before.

However, if an Object does have its own defined .toString() method then ToPrimitive (and thereby ToString) will use that method:

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

// Explicitly coercing `obj` to a string:
String(obj);
// expected result: "Joey Reyes rules"

// Calling the `.toString()` method:
obj.toString();
// expected result: "Joey Reyes rules"

Arrays have their own .toString() method:

var arr = [4, 2, 0];

// Explicitly coercing `arr` to a string:
String(arr);
// expected result: "4,2,0"

// Calling the `.toString()` method:
arr.toString();
// expected result: "4,2,0"

So there you have it. When an Object is coerced to a String:

  1. The ToString Abstract Operation relies on the ToPrimitive Abstract Operation.
  2. ToPrimitive looks for a [Symbol.toPrimitive] method on the Object, and if found, uses that method, otherwise
  3. ToPrimitive looks for a defined .toString() method on the Object, and if found, uses that method, otherwise
  4. ToPrimitive uses the generic .toString() method from Object.prototype.toString(), which returns the [[Class]] property. This property gets returned to ToString, resulting in the String "[object Object]".

We will revisit ToPrimitive momentarily within the context of Number coercion.

ToNumber Abstract Operation

When any non-Number value is coerced to a Number value, the conversion is handled by the ToNumber Abstract Operation, which is defined in Section 7.1.4 of the ECMAScript Language Specification. This generally occurs when a non-Number is used in a way that requires it to be a Number, such as a mathematical operation.

Primitive values will coerce as follows:

null coerces to 0:

Number(null);
// expected result: 0

undefined coerces to NaN:

Number(undefined);
// expected result: undefined

true coerces to 1:

Number(true);
// expected result: 1

false coerces to 0:

Number(false);
// expected result: 0

Strings will either convert naturally...

Number("420");
// expected result: 420

Number("-100");
// expected result: -100

...or they will convert to NaN:

Number("Joey Reyes Rules!");
// expected result: NaN

Number("Whatever, dude.");
// expected result: NaN

Unlike going from Number to a String, where -0 coerces to a positive representation, "0", going from a String to a Number preserves the negative representation:

Number("-0");
// expected result: -0

As before, Objects and Arrays are a more complicated matter, as they are first converted to their Primitive Value defined by the ToPrimitive Abstract Operation. The result of the primitive value conversion is then converted to a Number according to the rules above. Let's take another detour into the ToPrimitive Abstract Operation.

ToPrimitive Abstract Operation - Number Conversion

So we know that ToPrimitive first checks whether the Object in question has a [Symbol.toPrimitive] method defined. This is a place where we can define what value to return when the Object is being converted to a Number or being used in a context that requires a Number.

Let's take a look at our example from before:

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;
  },
};

Where before if we ran String(obj) we got the first half of the ternary operator within the [Symbol.toPrimitive] method, it should come as no surprise that if we run Number(obj) we get the second half of the ternary operator, which returns obj's a property, the Number value 100:

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;
  },
};

// Explicit Number Conversion
Number(obj);
/*
 * hint: number
 * 100
 */

// Implicit Number Conversion
console.log(obj - 200);
/*
 * hint: number
 * -100
 */

When we run this code, we see in both cases that hint is number. In the first example, property a, the Number value 100, gets returned. The second example demonstrates that with a properly configured [Symbol.toPrimitive] property, we can use obj like a part of any other regular mathematical operation.

As a quick aside, there is a third hint option, which is default. This hint option gets used when the operator isn't sure what type to expect:

obj + "100";
// hint: default
// '100100'

Here I tried to concatenate a String (that looks like a Number) to obj. [Symbol.toPrimitive] gets confused, uses default as the hint, and returns a "100" concatenated onto "100" for the value "100100". I'm not really going to explore this as it's beyond the scope of this post. The important thing here is that ToPrimitive first checks for a [Symbol.toPrimitive] method on the Object.

.valueOf() Fallback

Like before, not all Objects will have a [Symbol.toPrimitive] method, or we may work in a pre-ES6 environment that doesn't recognize this method. We saw before how ToPrimitive will fall back on either a defined or the generic .toString() method when the Object gets coerced to a String value. If the Object is getting coerced to a Number value, then ToPrimitive will fall back to a .valueOf() method instead.

By default, .valueOf() on an Object returns 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: [1, 2, 3],
 * }
 */

Within the context of a conversion, this method's returned value gets ignored. It's very confusing, but it seems as though this...

var obj = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
};
Number(obj);
// expected result: NaN

...is basically the same as:

Number(undefined);
// expected result: NaN

However, just like we can define a custom .toString() method on an Object to specify what happens when that Object coerces to a String, we can define our own .valueOf() method on an Object to specify what happens when that Object coerces to a Number:

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

Number(obj);
// expected result: 100

And that's really it for Object to Number conversions:

  1. The ToNumber Abstract Operation relies on the ToPrimitive Abstract Operation.
  2. ToPrimitive looks for a [Symbol.toPrimitive] method on the Object, and if found, uses that method, otherwise
  3. ToPrimitive looks for a defined .valueOf() method on the Object, and if found, uses that method, otherwise
  4. ToPrimitive uses the generic .valueOf() method from Object.prototype.valueOf(), which returns the Object itself. Within a conversion operation, this value doesn't get recognized, and gets treated as undefined. undefined, when returned to ToNumber, results in the NaN.

Controlling Object Coercion

If you haven't caught on by now, the [Symbol.toPrimitive] method is a new way of combining .toString() and .valueOf() methods.

This:

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

String(obj);
// expected result: '{ b: "Joey Reyes rules" }'

Number(obj);
// expected result: 100

Is basically the same as this:

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

String(obj);
// expected result: '{ b: "Joey Reyes rules" }'

Number(obj);
// expected result: 100

They're both ways of fine-tuning your control over the coercion of an Object. If you put them all together, [Symbol.toPrimitive] generally takes precedence within coercion operations, but the .toString() and .valueOf() methods may also be called in isolation:

var obj = {
  a: 100,
  b: "Joey Reyes rules",
  c: true,
  d: [1, 2, 3],
  [Symbol.toPrimitive](hint) {
    return hint === "string" ? "string symbol call" : (this.a + 320);
  },
  toString() {
    return ".toString() method call";
  },
  valueOf() {
    return this.a;
  },
};

String(obj);
// expected result: "string symbol call"

Number(obj);
// expected result: 420

obj.toString();
// expected result: ".toString() method call"

obj.valueOf();
// expected result: 100

ToBoolean Abstract Operation

When any non-Boolean value is coerced to a Boolean value, the conversion is handled by the ToBoolean Abstract Operation, which is defined in Section 7.1.2 of the ECMAScript Language Specification. It is this Abstract Operation that specifies which value types are "falsy" (i.e., will coerce to false) and which value types are "truthy" (i.e., will coerce to true). Compared to ToString and ToNumber, ToBoolean is relatively straightforward.

The "falsy" values are: undefined, null, +0, -0, NaN, and "" (empty String).

The "truthy" values are: all other values and types.

Or, to try to keep the structure consistent with the preceding sections:

null coerces to false:

Boolean(null);
// expected result: false

undefined coerces to false:

Boolean(undefined);
// expected result: false

0, +0, and -0 coerce to false:

Boolean(0);
// expected result: false

Boolean(+0);
// expected result: false

Boolean(-0);
// expected result: false

NaN coerces to false:

Boolean(NaN);
// expected result: false

"" (an empty String) coerces to false:

Boolean("");
// expected result: false

Everything else in JavaScript coerces to true:

Boolean("Joey Rules");
// expected result: true

Boolean(100);
// expected result: true

Boolean({});
// expected result: true

Boolean([]);
// expected result: true

Summary

ToString

Type String Representation
null "null"
undefined "undefined"
Boolean (example: true) "true"
Boolean (example: false) "false"
Number (example: 100) "100"
Really big Number (example: 1230000000000000000000) That number in exponent form (example: "1.23e+21")
Really small Number (example: .00000000000000123) That number in exponent form (example: "1.23e-15")
Object with a defined [Symbol.toPrimitive] method The value returned by the defined [Symbol.toPrimitive] for hint === "string"
Object with a defined .toString() method The value returned by the defined .toString() method
Object without defined [Symbol.toPrimitive] or .toString() methods "[object Object]"

ToNumber

Type Number Representation
null 0
undefined NaN
Boolean (true) 1
Boolean (false) 0
String (example: "100") 100
String (example: "hello") NaN
Object with a defined [Symbol.toPrimitive] method The value returned by the defined [Symbol.toPrimitive] for hint === "number"
Object with a defined .valueOf() method The value returned by the defined .valueOf() method
Object without defined [Symbol.toPrimitive] or .valueOf() methods NaN

ToBoolean

Type Boolean Representation
null false
undefined false
0, +0, or -0 false
NaN false
"" (empty String) false
All other values and types true

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