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:
- Converting non-
Stringvalues toStringvalues - Converting non-
Numbervalues toNumbervalues - Converting non-
Booleanvalues toBooleanvalues
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(0.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 theNumbervalue100b, which is theStringvalue"Joey Reyes rules"c, which is thebooleantrued, which is anArrayof theNumbers1,2, and3.
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 isreturn 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 isreturn 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:
- The
ToStringAbstract Operation relies on theToPrimitiveAbstract Operation. ToPrimitivelooks for a[Symbol.toPrimitive]method on theObject, and if found, uses that method, otherwiseToPrimitivelooks for a defined.toString()method on theObject, and if found, uses that method, otherwiseToPrimitiveuses the generic.toString()method fromObject.prototype.toString(), which returns the[[Class]]property. This property gets returned toToString, resulting in theString"[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: 420Number("-100");// expected result: -100
...or they will convert to NaN:
Number("Joey Reyes Rules!");// expected result: NaNNumber("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 isreturn 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 isreturn hint === "string" ? `{ b: "${this.b}" }` : this.a;},};// Explicit Number ConversionNumber(obj);/** hint: number* 100*/// Implicit Number Conversionconsole.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:
- The
ToNumberAbstract Operation relies on theToPrimitiveAbstract Operation. ToPrimitivelooks for a[Symbol.toPrimitive]method on theObject, and if found, uses that method, otherwiseToPrimitivelooks for a defined.valueOf()method on theObject, and if found, uses that method, otherwiseToPrimitiveuses the generic.valueOf()method fromObject.prototype.valueOf(), which returns theObjectitself. Within a conversion operation, this value doesn't get recognized, and gets treated asundefined.undefined, when returned toToNumber, results in theNaN.
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: 420obj.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: falseBoolean(+0);// expected result: falseBoolean(-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: trueBoolean(100);// expected result: trueBoolean({});// expected result: trueBoolean([]);// 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, aNumber, or aBoolean. - 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, andnull. - Scalar refers to a data type that has a single value or unit of data. The scalar data types in JavaScript are
Number,Boolean, andString. Contrast this withnullorundefined, 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 alsoArray) or aFunction. These types are made up of other types.
- 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
- 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-Stringvalues toStringrepresentations.ToNumber: The Abstract Operation that handles conversions of non-Numbervalues toNumberrepresentations.ToPrimitive: The Abstract Operation that handles conversions of complex values to primitive values.ToBoolean: The Abstract Operation that handles conversions of non-Booleanvalues toBooleanrepresentations.
Sources / Further Reading
- You Don't Know JS: Types & Grammar by Kyle Simpson
- Abstract Operations in ECMAScript 2023 Language Specification by tc39
- Type Conversion Abstract Operations in ECMAScript 2023 Language Specification by tc39
- ToString Abstract Operation in ECMAScript 2023 Language Specification by tc39
- ToNumber Abstract Operation in ECMAScript 2023 Language Specification by tc39
- ToPrimitive Abstract Operation in ECMAScript 2023 Language Specification by tc39
- ToBoolean Abstract Operation in ECMAScript 2023 Language Specification by tc39
- Object to primitive conversion on javascript.info