Explicit Coercion
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:
- Converting non-
Stringvalues toStringvalues - Converting non-
Numbervalues toNumbervalues - Converting non-
Booleanvalues toBooleanvalues
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).
What is Explicit Coercion?
Explicit Coercion is a coercion operation where it's very clear from the code what value type is getting converted to another value type and why. This is contrasted with Implicit Coercion, which 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), and thereby it's less clear what value type is converted to another value type and why.
These are, of course, relative terms - a developer who is very experienced with string concatenation may find that what's commonly called an "implicit coercion" is actually a very clear operation. What I will cover in this blog post (and later in an Implicit Coercion post) will fall in line with what the larger JavaScript community generally considers to be explicit and implicit.
Another way to break it down that may be helpful is to think of "Explicit Coercion" as something more like "Verbose Coercion", where the mechanism that triggers the type conversion operation is readable in the code. In my last post, Coercion: ToString, ToNumber, and ToBoolean, the majority of my examples looked something like the following:
String(420);// expected result: "420"Number("100");// expected result: 100Boolean(0);// expected result: falseBoolean(1);// expected result: true
The first thing to note here is the usage the built-in functions String(), Number(), and Boolean() (these are sometimes referred to as "native constructors", although in this case we specifically are not also using a new keyword). The function name corresponds to the type that we want to convert the value to. The function then accepts as its sole argument the value that we are converting.
So in this example, String() gets a non-String value, the Number 420; Number() gets the non-Number value, the String "100"; and Boolean() gets non-Boolean values, the Numbers 0 and 1. The functions then go about converting the value they are given based on the rules of the Abstract Operations covered in the last blog post, ToString, ToNumber, (sometimes) ToPrimitive, and ToBoolean.
The pattern here is the important thing: code that uses these built-in functions is generally considered to be "Explicit", since the function spells out quite plainly the expected output type, and the argument it's given also makes it plain to see the type and value of the data that we are converting. That said, there are a few other ways to make these conversions, and we will explore those now.
Explicitly Coercing to a String
There are two main ways to explicitly coerce a value to a String representation:
- Using the global
String()function - Using the
.toString()object method
The Global String() Function
As we have seen previously, String() typically coerces primitive values as you might expect.
null coerces to "null":
String(null);// expected result: "null"
undefined coerces to "undefined":
String(undefined);// expected result: "undefined"
true coerces to "true" and false coerces to "false":
String(true);// expected result: "true"String(false);// expected result: "false"
Numbers coerce to their String equivalents unless they are really big or really small, in which case they are represented exponentially:
String(420);// expected result: "420"String(1230000000000000000000);// expected result: "1.23e+21"String(0.00000000000000123);// expected result: "1.23e-15"
-0 will coerce to a positive 0 representation:
String(-0);// expected result: "0"
When coercing an Object to a String, the conversion first looks for a [Symbol.toPrimitive] method on the Object, and if found uses that:
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" }'*/
The .toString() Object Method
If a [Symbol.toPrimitive] method is not found, then ToPrimitive looks for a .toString() method on the Object, and if found uses that:
var obj = {a: 100,b: "Joey Reyes rules",c: true,d: [1, 2, 3],toString() {return this.b;},};String(obj);// 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],};String(obj);// expected result: "[object Object]"
We can also explicitly call .toString() on some of the primitives, but there are some differences to note. The Number.prototype and Boolean.prototype objects have their own defined .toString() methods that override the Object.prototype.toString() method. Because of this, when you call .toString() on a Number or a Boolean, the value is "boxed" in its prototype Object wrapper and it uses its specific type's .toString() method:
var num = 420;num.toString();// expected result: "420"var bool1 = true;bool1.toString();// expected result: "true"var bool2 = false;bool2.toString();// expected result: "false"
On a note related to Number.prototype.toString(), Number.prototype also has .toExponential(), .toFixed(), and .toPrecision() methods that also may be used with a Number to return a String representation of that Number. These methods will have their own specific parameters and work within their own sets of rules to return a String representation according to the proper exponential, fixed, or specified precision expectations:
var num = 1234567890;num.toExponential();// expected result: "1.23456789e+9"num.toFixed(2);// expected result: "1234567890.00"num.toPrecision(4);// expected result: "1.235e+9"
Whether you would consider these methods to be clear enough that they return a String from a non-String value is really up to you. I include them here because they seem related to Number.prototype.toString() (which is widely considered to be rather explicit), but it just starts to show how fuzzy the terms "explicit" and "implicit" really are.
Array.prototype also has its own .toString() method that can be used in a similar manner. This method internally calls the .join() method, returning a single String containing each element within the Array, separated by commas:
var arr = ["Joey", "Reyes", "rules"];arr.toString();// expected result: "Joey,Reyes,rules"
An empty Array will return an empty String:
var arr = [];arr.toString();// expected result: ""
Function.prototype also has its own .toString() method, which generally returns a String representing the source code of the function:
function doSomething(a) {return a * a;}doSomething.toString();// expected result: "function doSomething(a) {\n return a * a;\n}"
This produces the same behavior we see if we pass a Function to the global String() function:
function doSomething(a) {return a * a;}String(doSomething);// expected result: "function doSomething(a) {\n return a * a;\n}"
Symbol.prototype similarly has its own .toString() method, which returns a String representing the source code of the specified Symbol:
var sym = Symbol("howdy");sym.toString();// expected result: "Symbol(howdy)"
As you might expect, the output is the same as if we passed the Symbol to the global String() function:
var sym = Symbol("howdy");String(sym);// expected result: "Symbol(howdy)"
null and undefined on the other hand do not have .toString() methods, nor do they inherit the method from Object.prototype.toString(). Because of this, if you call .toString() on null or undefined, you get an error:
var nullThing = null;nullThing.toString();// expected result: TypeErrorvar undefinedThing = undefined;undefinedThing.toString();// expected result: TypeError
Explicitly Coercing to a Number
There are three main ways to explicitly coerce a value to a Number representation:
- Using the global
Number()function - Using the
parseInt()andparseFloat()functions - Using the
+or-unary operators
The Global Number() Function
Number() also typically coerces primitive values as you might expect.
null coerces to 0:
Number(null);// expected result: 0
undefined coerces to NaN:
Number(undefined);// expected result: NaN
true coerces to 1 and false coerces to 0, which are "falsy" values (more on that later):
Number(true);// expected result: 1Number(false);// expected result: 0
Strings will coerce to their Number equivalents if possible...
Number("420");// expected result: 420
...otherwise they will coerce to NaN:
Number("four hundred twenty");// 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 with coercing an Object to a String, when coercing an Object to a Number, the conversion first looks for a [Symbol.toPrimitive] method on the Object, and if found uses that:
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;},};Number(obj);/** expected output:* hint: number* 100*/
If that's not found, then it looks for a .valueOf() method on the Object, and if found uses that.
var obj = {a: 100,b: "Joey Reyes rules",c: true,d: [1, 2, 3],valueOf() {return this.a;},};Number(obj);// expected result: 100
Otherwise it uses the generic .valueOf() method from Object.prototype.valueOf(), which returns the Object itself, a value that gets ignored by the ToPrimitive Abstract Operation, which ultimately returns NaN.
var obj = {a: 100,b: "Joey Reyes rules",c: true,d: [1, 2, 3],};Number(obj);// expected result: NaN
As you might expect, Boolean.prototype and String.prototype have their own .valueOf() methods that override Object.prototype.valueOf(), but these don't come into play in coercing these values to Number representations as much as .toString() comes into play with String coercions.
Passing a Function to the Number() global function will produce NaN:
function doSomething(a) {return a * a;}Number(doSomething);// expected result: NaN
I believe this is because, once again, ToNumber tags in ToPrimitive, which looks for a [Symbol.toPrimitive], doesn't find one, then uses the result of either .valueOf() (which returns the function itself), or .toString() (which returns a String representation of the function itself). In either case, ToPrimitive passes an unrecognized value (the Function itself) or a String up to ToNumber, and either of those values coerces to NaN.
Symbol.prototype has its own .valueOf() method, which returns the primitive value of the Symbol object as a Symbol data type:
var sym = Symbol("howdy");sym.valueOf();// expected result: Symbol(howdy)
However, this doesn't really come into play in this discussion, as passing a Symbol to the global Number() function throws a TypeError:
var sym = Symbol("howdy");Number(sym);// Uncaught TypeError: Cannot convert a Symbol value to a number
The parseInt() and parseFloat() Functions
An operation that's related to coercing Strings values to Number representations is the parseInt() function, which is specifically used to parse a String argument to a Number. The main difference between Number(String) and parseInt(String) is that parseInt() is more tolerant of non-Number values being passed to it:
var str1 = "420";var str2 = "420px";var str3 = "px420";Number(str1);// expected result: 420Number(str2);// expected result: NaNNumber(str3);// expected result: NaNparseInt(str1);// expected result: 420parseInt(str2);// expected result: 420parseInt(str3);// expected result: NaN
Where the global Number() function will always return NaN if any non-numeric value is found in the String that it is converting, parseInt() works from left to right, parsing each individual character within the String into the Number that it returns, but stops when it reaches the first non-numeric value. An exception here is leading space characters:
var str4 = " 420 123";parseInt(str4);// expected result: 420
There is a lot more to parseInt() than just this behavior, though, including passing a specified radix argument. This topic is beyond the scope of this blog post (as well as my understanding at this time), so I will refer you to MDN's parseInt() article for more details there.
Another similar function is the parseFloat() function, which accepts a String (or converts its argument to a String) and returns a floating point number:
var str1 = "42.0123";var str2 = "42.0123px";var str3 = "px42.0123";var str4 = " 42.0 123";Number(str1);// expected result: 42.0123Number(str2);// expected result: NaNNumber(str3);// expected result: NaNNumber(str4);// expected result: NaNparseFloat(str1);// expected result: 42.0123parseFloat(str2);// expected result: 42.0123parseFloat(str3);// expected result: NaNparseFloat(str4);// expected result: 42
Something to note about parseInt() and parseFloat() is that they are really designed to convert String values to Number (or floating point) representations. They can accept non-String values, but they will first run that value through the ToString abstract operation. On an academic level, this additional step might actually fall into the definition of "implicit" coercion since it's a hidden coercion that's the side-effect of another operation. More practically speaking, ToString can just produce some unexpected behavior, so it's an area where caution should be taken.
Another thing to note is that because parseInt() and parseFloat() are tolerant of non-numeric characters and takes steps to parse out a Number from the start of the String, these two functions are a bit slower and less performant than Number().
The + or - Unary Operators
The third method for converting a non-Number value to a Number representation is with either the + or - Unary Operators. There's debate over whether this method is truly explicit or implicit, but I'm including it here because using the operator in a certain manner is intended for Number coercion.
The way that this method works is that you prepend the operand that you want to coerce to a Number with a + sign. This pattern is the same as passing the value you want to convert to Number():
+"420";// expected result: 420+"four hundred twenty";// expected result: NaN
- works in a similar way, but it flips the sign of the Number that it returns:
-"420";// expected result: -420-"four hundred twenty";// expected result: NaN
The problem with this approach is that + and - are rather overloaded symbols in JavaScript. Not only can they be used in this manner for Number coercion, they can also be used to concatenate Strings...
var str = "Joey";str += " rules ";str += 420;console.log(str);// expected result = "Joey rules 420"
...or they can be used to increment/decrement Number values...
var coolNumber = 420;console.log(++coolNumber);// expected result: 421console.log(--coolNumber);// expected result: 420
...or (of course) they can be used in addition and subtraction operations:
var num = 210 + 210;console.log(num);// expected result: 420num -= 100;console.log(num);// expected result: 320
This can all get especially confusing when you start mixing and matching these operations:
var numericString = "320";var superCoolNumber = 100 + +numericString;console.log(superCoolNumber);// expected result: 420
There are all kinds of nightmarish ways that these operators can get combined to make what is otherwise a straightforward explicit conversion operation feel anything but straightforward. Personally I prefer to avoid this method for Number coercion, but large parts of the JavaScript community feel that it falls within the "explicit" category, so I'm including it here (along with all of these qualifiers about how its usage may be confusing).
Explicitly Coercing to a Boolean
There are two main ways to explicitly coerce a value to a Boolean representation:
- Using the global
Boolean()function - Using the
!unary operator
Both of these approaches will return a Boolean value according to the rules set out in the ToBoolean Abstract Operation.
The following values will coerce to false, making them "falsy":
undefinednull+0-0NaN""(an emptyString)
All other values and types will return true, making them "truthy".
The Global Boolean() Function
This approach should be fairly straightforward by now. We just pass the value we want to convert to a Boolean to the global Boolean() function.
null coerces to false:
Boolean(null);// expected result: false
undefined coerces to false:
Boolean(undefined);// expected result: false
+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 coerces to true:
Boolean("Joey Rules");// expected result: trueBoolean(100);// expected result: trueBoolean({});// expected result: trueBoolean({ a: 420 });// expected result: trueBoolean([]);// expected result: trueBoolean(["Joey", "Reyes", "rules"]);// expected result: truefunction doSomething(a) {return a * a;}Boolean(doSomething);// expected result: truevar sym = Symbol("howdy");Boolean(sym);// expected result: true
Pretty straightforward stuff.
The ! Unary Operator
The other route for coercing to a Boolean (that really is more commonly encountered than the global Boolean() function) is the ! unary negate operator. Prepending the value you want to convert with ! will coerce it to a Boolean...
!null;// expected result: true!undefined;// expected result: true!0;// expected result: true!"Joey rules";// expected result: false![];// expected result: false!["Joey", "Reyes", "rules"];// expected result: falsevar sym = Symbol("howdy");!sym;// expected result: false
...however, we see here that this approach actually flips the value from "truthy" to "falsy" or vice versa, hence why it's called the "negate" operator.
Because of this, what will commonly be done is to use two ! operators, !! - sometimes called the double-negate operator - to produce the value's actual Boolean representation:
!!null;// expected result: false!!undefined;// expected result: false!!0;// expected result: false!!"Joey rules";// expected result: true!![];// expected result: true!!["Joey", "Reyes", "rules"];// expected result: truevar sym = Symbol("howdy");!!sym;// expected result: true
Summary
Explicit Coercion is a coercion operation where it's very clear from the code what value type is getting converted to another value type and why.
We can explicity coerce non-String values to String representations using the global String() function and the .toString() method.
We can explicitly coerce non-Number values to Number representations using the global Number() function, the parseInt() and parseFloat() functions, and the + and - unary operators.
We can explicitly coerce non-Boolean values to Boolean representations using the global Boolean() function and the ! unary operator.
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
- Explicit
StringCoercion Operations:- String() Constructor on MDN
- Object.prototype.toString() on MDN
- Number.prototype.toString() on MDN
- Number.prototype.toExponential() on MDN
- Number.prototype.toFixed() on MDN
- Number.prototype.toPrecision() on MDN
- Boolean.prototype.toString() on MDN
- Array.prototype.toString() on MDN
- Function.prototype.toString() on MDN
- Symbol.prototype.toString() on MDN
- Explicit
NumberCoercion Operations: - Explicit
BooleanCoercion Operations: