DEV Community

Cover image for JavaScript Coercion : Beyond Basics
Faisal
Faisal

Posted on • Originally published at thefaisal.dev

JavaScript Coercion : Beyond Basics

In JavaScript, we often see implicit type conversion in our code which occurs due to abstract operation. In JS, we use the term coercion for what's commonly known as type conversion. When we consider conversion and coercion, it's best to see them as interchangeable, especially in the context of JavaScript.

Coercion is a weird topic in javascript, that is why many tends to ignore this topic. However, we can't ignore something that behaves counterintuitively. We will start by exploring how abstract operations and coercion occur implicitly. Following that, we'll discuss why they are important.

Here one thing to note that, abstract operations are not like regular functions that you explicitly call in your code. Instead, they represent conceptual operations that help describe the behavior of certain language features.

While there may be internal methods or mechanisms within the JavaScript engine that handle the tasks associated with abstract operations, the key idea is that these operations are more about defining behavior and functionality at a higher level of abstraction.

ToPrimitive Operation

The abstract operation that we will talk about first is called ToPrimitive or primitive coercion process. The ToPrimitive operation converts a value to a primitive value, either a string, number, or a default value. It is often invoked implicitly by JavaScript when an object is used in a context where a primitive value is expected.

Let's say we are performing an operation where it requires a value to be primitive. Now, if we don't have a primitive in there, we need to turn that into a primitive.

If we have a non-primitive entity that needs to be transformed into a primitive one, conceptually, what it must follow is a series of algorithmic steps. This set of steps is referred to as ToPrimitive, almost as if it were a function that could be called.

The ToPrimitive operation takes an optional type hint. The hint helps to figure out what it needs to be converted to. Let's say if we are doing a numeric operation and if there needs to run the ToPrimitive operation, the hint will be something like this - 'I would like it to be a number'. It's just an implicit process where JavaScript understands what type it needs to be. If we do something string-based, it sends a hint as a string. These are the two basic hints. And if it can't figure out the hint, it returns whatever the best primitive value it can return in that context.

One important thing is that -

JavaScript algorithms inherently follow a recursive pattern. For example, if what returns from ToPrimitive isn't a primitive type value but instead another non-primitive thing, ToPrimitive will be called again. It will keep getting invoked until we obtain an actual primitive. If it can't return a primitive value, it eventually results in an error.

The ToPrimitive operation relies on two methods - valueOf and toString. These can be available on any non-primitive value in JavaScript, be it an object, function, array, or any other non-primitive type. These two methods/functions play a crucial role in the process of converting non-primitive values to primitive ones.

Remember, ToPrimitive operation takes an optional hint.

Now if the hint is number, then ToPrimitive invokes the valueOf method first and see what it returns. If valueOf method gives a primitive, then we're done. However, if it doesn't give a primitive or if the method itself doesn't exist, then it tries the second method - toString.

If toString method also fails to give a primitive, it usually leads to an error.

JavaScript calls the toString method to convert an non primitive to a primitive value. We rarely need to invoke the toString method ourselves; JavaScript automatically invokes it when encountering an object where a primitive value is expected. MDN

If the hint is string, still both valueOf and toString method occurs, but in reverse order. In terms of string hint, if it ToPrimitive operation tries to make a non primitive into a primitive, toString method invoke first. Then, if it gives us a primitive like a string, we will just use that.

So, no matter what the hint is, if we are trying to use something that is not primitive where it needs to be primitive, like in some math or concatenation, then it goes through ToPrimitive algorithm and it ends up either invoking the valueOf method or toString method.

Diving deep into valueOf method

Let's take an object - const obj = {value: 4}.

Now, obj + 3 returns us '[object Object]3' as output. But, shouldn't it be 7? Since the object's value is giving a number hint, it should already be turned into a number, right? Then, why is it behaving this way?

To figure out that answer, we need to understand that, under the hood, JavaScript is still running the ToPrimitive operation on our written obj to convert it to work in primitive operations - like adding value. And, since it's taking a number as a hint - as we know, it is running the valueOf method first. But, the problem is, the default valueOf method of JavaScript, which gets inherited from Object.prototype, is basically useless in this case because the valueOf method inherited from Object.prototype returns the object itself (this).

And, since the ToPrimitive algorithm is recursive, it runs again, and sees that the valueOf method is not working, so, it falls back into the toString method. And when the toString method runs on any object type, it returns - "[object Object]"

So, our object basically is turning into a string first which is "[object Object]", Then, when we use the + operator between two string types, it just concatenates them.

obj + 3

"[object Object]" + 3

"[object Object]3"
Enter fullscreen mode Exit fullscreen mode

Many built-in objects override the valueOf method to return an appropriate primitive value. When you create a custom object, you can override valueOf to call a custom method so that your custom object can be converted to a primitive value when needed.

For example -

const obj = {
    value: 4,
}

obj.valueOf() // {value: 4}
Enter fullscreen mode Exit fullscreen mode

It is returning the object itself(this). But, you can create a function to be called in place of the default valueOf method. Your function should take no arguments, since it won't be passed any when called during type conversion.

const obj = {
    value: 4,
    valueOf: function() {
        return this.value;
    }
};

obj.valueOf() // 4
Enter fullscreen mode Exit fullscreen mode

In this case, it is just returning the value. And, if we try to execute something where our written obj will need to be a primitive type, we will see that it will behave as expected -

obj + 3 will return 7.

Why? Because, since the adding operation is invoking the ToPrimitive operation with a number hint, so, the valueOf method is running first. But, now, our obj has its own custom valueOf method, so it's not executing the inherited valueOf method from Object.prototype which returns this object itself. Instead, for our custom valueOf method, it's giving us the actual number form of the value, since we are getting a primitive type already, it's not falling into the toString method.

ToString Operation

Here are some values and their corresponding string values after performing the ToString operation on them:

  • null => "null"
  • undefined => "undefined"
  • true => "true"
  • false => "false"
  • 3.14 => "3.14"
  • 0 => "0"
  • -0 => "0"

The ToString operation is executed during abstract operations involving implicit type conversion.

When ToString is applied to object types like arrays, it returns values as demonstrated in the following examples:

  • [] => ""
  • [1, 2, 3] => "1,2,3"
  • [null, undefined] => ","
  • [ [ [ ], [ ] ], [ ] ] => ",,"
  • [,,,] => ",,,"

For objects:

  • {} => "[object Object]"
  • {a: 2} => "[object Object]"

We can modify the default behavior of toString by implementing a custom toString method in our object. It will be called when needed:

{toString() { return "A"; }} => "A"
Enter fullscreen mode Exit fullscreen mode

ToNumber Operation

There are a lot of corner cases involved in terms of ToNumber.

If we need to do something numeric and we don't have a number, JS invokes the ToNumber abstract operation. Some conversions are pretty straightforward, while others are counterintuitive.

  • "" => 0

    • An empty string returns 0. Empty string should be NaN, right? Empty means there is nothing. How can nothing be 0? That's weird. It's not only a weird case in JavaScript; it creates a lot of other problems in terms of [[Coercion]] in JS.
  • " 003 " => 3

    • If there are any empty spaces, JS removes those and returns the corresponding number.

Others are pretty straightforward:

  • "0" => 0
  • "-0" => -0
  • "3.14" => 3.14
  • "0.0" => 0
  • "." => NaN

    • "." turns into NaN, which is as it should.
  • "0xaf" => 175

    • Hexadecimal string turns into their corresponding numbers.
  • false => 0

  • true => 1

    • Within the context of programming, maybe it's a sensible thing to turn false and true into 0 and 1, as that's how computers treat them. But, it creates an issue. false and true should turn into NaN. We will see the reason later when we discuss the corner cases.
  • null => 0

  • undefined => NaN

    • Null and undefined both should be NaN, but one turns into 0, and another turns into NaN. That's interesting. For those who think that converting null uses the valueOf method of Object because null is an object, one interesting thing is that null doesn't inherit methods from Object.prototype.

ToNumber Operation on Non-Primitive

When we run ToNumber on a non-primitive or object, it evokes the ToPrimitive operation with the number hint. In ToPrimitive operation, it first runs the valueOf method, which is inherited from the Object.prototype by default, and it returns the object itself (this). JS ignores that, and it falls into the toString method.

Since it falls into tpString, no matter if our hint is a number or not, the value turns into a string. So, we can think of the numberification of an object as the stringification of it.

There are cases where we want something to be a primitive number, and we can end up with a primitive string for the internal process of valueOf. We need to be aware of that.

If it's empty like [] or {}, then the same things happen. It evokes the ToPrimitive, and the valueOf returns the empty parenthesis - [] or {}, then it falls into toString as usual.

Some more ToNumber operations in terms of arrays:

  • [""] => 0
  • ["0"] => 0
  • ["-0"] => -0
  • [null] => 0
  • [undefined] => 0
  • [1,2,3] => NaN
  • [[[]]] => 0

Few important things to notice here:

  • [""] falls into ToString, and then it returns an empty string. If ToNumber tries to convert an empty string, it converts it to 0, which is our old problem as we saw earlier.

  • Similar things happen for [[[]]] - first, it turns into "" by toString, then, it converts into a number, which is 0. Emptiness shouldn't be 0 in this case.

  • We see that [null] and [undefined] return 0 in the ToNumber operation, which doesn't make sense. We have seen different behavior in their case when we have tried to convert primitive null and undefined. But, in this case, they are returning an identical value, which is 0. Why? In this case, this is a non-primitive thing in both cases, and as it's non-primitive, ToPrimitive evokes, and the valueOf method runs first. It returns this, then it falls into the toString method, and remember what toString does on a non-primitive with null or undefined? It turns into an empty string. And then, that empty string turns into 0. Our root problem again.

  • {..} => NaN

    • If it's an object {}, then the same thing happens. ToPrimitive evokes, then it falls into toString. ToString returns "[object Object]", which is definitely not a representation of a number, so it returns NaN.
    • But, if we add our custom valueOf method, it doesn't fall into toString, and it returns what we want to be returned.

ToBoolean Operation

This is pretty straightforward. We just need to remember all the falsy values, which will always return false if we try to run ToBoolean operation on them. Everything else returns true.

Falsy values: "", 0, -0, null, NaN, false, undefined Truthy values: Everything else.

One thing is important to notice here:

When we try to convert anything to Boolean, it doesn't evoke any other operation under the hood, like ToNumber. It just checks its truthiness or falsiness.

That's why if we convert [ ] to a boolean, it returns true, and if we convert "" to a boolean, it returns false. Because an empty string is a falsy value, [ ] is not.

If it would evoke the toString method under the hood, that would convert [] to an empty string, and then we would get false because of that empty string. But, in this case, that is not happening.

Corner Cases

Every language has type conversion. We can't just avoid coercion in JavaScript for its corner cases. No language is free from various kinds of corner cases. We just need to be aware of those corner cases and learn how to work around them. Here are some of the corner cases we need to be aware of -

Number("") // 0  (!!!)
Number("   \t\n") // 0  (!!!)
Number(null) // 0  (!!!) should be NaN!
Number(undefined) // 0 
Number([]) // 0
Number([1,2,3]) // NaN


Number([null]) // 0  -> !!!
Number([undefined]) // 0  -> !!!

Number({})  // NaN


String(-0)  // "0"   -> what! should be "-0"!
String(null)  // "null"  -> good!
String(undefined)  // "undefined"  -> good!
String([null])  // ""  -> !!!
String([undefined])  // ""  -> !!!

Boolean(new Boolean(false)); // true  -> what!!!
Enter fullscreen mode Exit fullscreen mode

One of the root causes of corner cases occurring in coercion in JS is the conversion of "" to 0. We have already seen what kind of problems it can cause. Many coersions would just be gone if an empty string wouldn't turn into 0 in number conversion; if it would be NaN. Whatever, we can't do anything about it now because of backward compatibility.

Another important corner case is true becoming 1, and false becoming 0 in terms of converting to number operation. Number(true) -> 1 Number(false) -> 0

Though it is an intuitive thing to us. Let's see how it creates a problem. 1 < 2 -> true; Okay. 2 < 3 -> true; It's also okay. 1 < 2 < 3 -> true. Okay. You are thinking - it's exactly working as it should be.

But that is not the case; what is happening under the hood is it first returns true for the first part. Then, true converts into 1, since we are performing a numeric operation. Then, 1 < 3 obviously returns true.

(1 < 2) < 3
(true) < 3
1 < 3
Enter fullscreen mode Exit fullscreen mode

We see how this can create a problem.

7 > 5 // true

5 > 3 // true

7 > 5 > 3 // false --> !!!
Enter fullscreen mode Exit fullscreen mode

Let's see, which is actually happening.

(7 > 5) > 3
(true) > 3
1 > 3 // false
Enter fullscreen mode Exit fullscreen mode

Since 1 is not greater than 3, it returns false. Now, we understand how converting true and false to 1 and 0 creates problems. It would be solved if it were just NaN.

Some of us hate coercion because of it's weird corner cases. But, we can't ignore coercion. We use coercion all the time.

Practical Use Cases of Coercion

Coercion is a weird topic in JavaScript. But, we can't avoid it either. It turns out, we all deal with coercion all the time! Let's see some examples.

var numPeople = 19;

console.log(`There are ${numPeople} people out there.`);

// There are 19 people out there.
Enter fullscreen mode Exit fullscreen mode
var firstPart = "There are ";
var numPeople = 19;
var secondPart = " people out there.";

console.log(firstPart + numPeople + secondPart);

// There are 19 people out there.
Enter fullscreen mode Exit fullscreen mode

It turns out that if we use the + operator and any of the values is a string, it converts the entire expression into a string. This is known as Operator Overloading. Our number is implicitly converting into a string. Some may want to make it explicit:

var numPeople = 19;

console.log(`There are ${numPeople + ""} people out there.`);

// There are 19 people out there.
Enter fullscreen mode Exit fullscreen mode

or

var numPeople = 19;

console.log(`There are ${[numPeople].join("")} people out there.`);

// There are 19 people out there.
Enter fullscreen mode Exit fullscreen mode

or

var numPeople = 19;

console.log(`There are ${numPeople.toString()} people out there.`);

// There are 19 people out there.
Enter fullscreen mode Exit fullscreen mode

or

var numPeople = 19;

console.log(`There are ${String(numPeople)} people out there.`);

// There are 19 people out there.
Enter fullscreen mode Exit fullscreen mode

Writing explicit code is not a good decision always.

Some may ask, what about converting a string to a number? We do that too. What about form data? We all need to work with form data as JavaScript developer.

function addOnePeople(numPeople) {
    return numPeople + 1;
}

addOnePeople(peopleInputElement.value); // get people number from form

// "191" - WHAT!!!!
Enter fullscreen mode Exit fullscreen mode

So, again, we need to convert it into a number first! There are two ways. First one -

function addOnePeople(numPeople) {
    return numPeople + 1;
}

addOnePeople(+peopleInputElement.value);

// 20
Enter fullscreen mode Exit fullscreen mode

or we can use the Number function, which is the more preferred way.

function addOnePeople(numPeople) {
    return numPeople + 1;
}

addOnePeople(Number(peopleInputElement.value));

// 20
Enter fullscreen mode Exit fullscreen mode

How about reducing the input value?

function ignoreOnePeople(numPeople) {
    return numPeople - 1;
}

ignoreOnePeople(peopleInputElement.value);

// 18
Enter fullscreen mode Exit fullscreen mode

Since numPeople is a string and we are trying to perform an operation where a number is needed, the ToPrimitive process is triggered, and numPeople converts into a number automatically. That's why we don't need to explicitly mention that. We even need to check truthy and falsy values all the time.

if (peopleInputElement.value) {
    numPeople = Number(peopleInputElement.value);
}
Enter fullscreen mode Exit fullscreen mode

Again, if we aren't aware of the corner case of Boolean, we may face a bug here. What if the input element contains a bunch of white spaces? That won't be a falsy value!

Boxing

We have seen that non-primitive values don't have methods in them. If that's the case, how can we access methods in a string like somestring.length?

It's called Boxing. It's a form of implicit coercion, although it doesn't occur in the same process as abstract operations do.

It's like when JavaScript sees that you are trying to run a method on a non-primitive value, JavaScript does you a favor. It thinks, "Okay, this person is trying to run an operation as if it's a primitive value (object); let's turn it into that and make their life easier!" And then, it converts it into the corresponding object representation of the string.

Maybe from this, the falsy notion comes: In JavaScript, everything is an object. No, just because it is converting into an object-like structure under the hood doesn't mean it's an object. Things can behave as similar, but that doesn't make them similar.

Two things are pretty different.

A significant part of my motivation to delve deeply into JavaScript was influenced by Kyle Simpson. I conclude here by sharing some of his philosophy on "coercion."

  • You don't deal with corner cases by avoiding coercions.
  • You have to adopt a coding style that makes value types plain and obvious.
  • A quality JS program embraces coercions, making sure the types involved in every operation are clear. Thus, corner cases are safely managed.
  • JavaScript's dynamic typing is not a weakness, it's one of it's strong qualities.
  • Implicit doesn't mean magic. It means abstraction. So, that it can hide unnecessary details, re-focusing the reader and increasing clarity.
    • We can simiply depends on implicit type coercion rather than always describing type convertion explicitely every time, which will decrease readability.

It's "Useful", when the reader is focused on what's important. It's "Dangerous", when the reader can't tell what will happen. It's "Better", when the reader understands the code.

It's "Irresponsible" to knowingly avoid usage of a feature that can improve code readability.

And it's always better to learn how things work under the hood!

Happy Learning!

If you enjoyed reading this, you can connect with me on Twitter or check out my other articles.

Additional Resources

JavaScript Data Structures - MDN

valueOf - MDN

valueOf in JavaScript - ECMA

Abstract Operations: To Primitive - ECMA

You Don't Know JS by Kyle Simpson

Top comments (1)

Collapse
 
efpage profile image
Eckehard

We can make our code more resillient by adding some parameter tests at the start of function bodies. But I assume, we will face some problems due to unexpected type conversions.

Are there any recommendations how to do parameterchecks in Javascript?