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-
String
values toString
values - Converting non-
Number
values toNumber
values - Converting non-
Boolean
values toBoolean
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 Object
s, or iterators on Array
s or String
s 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"
Number
s also generally convert naturally...
String(420);// expected result: "420"
String(-100);// expected result: "-100"
...but very large and very small Number
s 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"
Object
s 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 theNumber
value100
b
, which is theString
value"Joey Reyes rules"
c
, which is theboolean
true
d
, which is anArray
of theNumber
s1
,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 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 Object
s have a generic .toString()
method they inherit from Object.prototype.toString()
. If no other .toString()
method is found on theObject
, 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"
Array
s 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
ToString
Abstract Operation relies on theToPrimitive
Abstract Operation. ToPrimitive
looks for a[Symbol.toPrimitive]
method on theObject
, and if found, uses that method, otherwiseToPrimitive
looks for a defined.toString()
method on theObject
, and if found, uses that method, otherwiseToPrimitive
uses 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: 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, Object
s and Array
s 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 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 Object
s 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
ToNumber
Abstract Operation relies on theToPrimitive
Abstract Operation. ToPrimitive
looks for a[Symbol.toPrimitive]
method on theObject
, and if found, uses that method, otherwiseToPrimitive
looks for a defined.valueOf()
method on theObject
, and if found, uses that method, otherwiseToPrimitive
uses the generic.valueOf()
method fromObject.prototype.valueOf()
, which returns theObject
itself. 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: 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
, 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 withnull
orundefined
, 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-String
values toString
representations.ToNumber
: The Abstract Operation that handles conversions of non-Number
values toNumber
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 toBoolean
representations.
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