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:

  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).

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: 100

Boolean(0);
// expected result: false

Boolean(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:

  1. Using the global String() function
  2. 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(.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 is
    return 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: TypeError

var 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:

  1. Using the global Number() function
  2. Using the parseInt() and parseFloat() functions
  3. 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: 1

Number(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 is
    return 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: 420
Number(str2);
// expected result: NaN
Number(str3);
// expected result: NaN

parseInt(str1);
// expected result: 420
parseInt(str2);
// expected result: 420
parseInt(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.0123
Number(str2);
// expected result: NaN
Number(str3);
// expected result: NaN
Number(str4);
// expected result: NaN

parseFloat(str1);
// expected result: 42.0123
parseFloat(str2);
// expected result: 42.0123
parseFloat(str3);
// expected result: NaN
parseFloat(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: 421

console.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: 420

num -= 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:

  1. Using the global Boolean() function
  2. 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":

  • undefined
  • null
  • +0
  • -0
  • NaN
  • "" (an empty String)

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: 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 coerces to true:

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

Boolean(100);
// expected result: true

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

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

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

Boolean(["Joey", "Reyes", "rules"]);
// expected result: true

function doSomething(a) {
  return a * a;
}
Boolean(doSomething);
// expected result: true

var 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: false

var 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: true

var 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, 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