Detailed Guide to Coercion in Javascript

Detailed Guide to Coercion in Javascript

·

15 min read

Type Coercion can be defined as implicit conversion of one type of value into another type of value. Javascript coerces one type of value into another when it sees a value of one data type in a context where it expected a value of a different data type.

Javascript developers have mixed feelings about coercion in Javascript, some think of it as a powerful feature and they try to take advantage of coercion wherever possible, whereas some simply hate it because it makes their code produce unexpected results.

Whether you like coercion or not, if you write javascript code, sooner or later you are going to come across a code that involves type coercion. If you do not understand coercion well enough, it will leave you baffled. To avoid such a situation, it's essential to understand how coercion works and the rules that javascript follows to convert one type of value into another.

Before explaining type coercion, it is important to note that it is different from type conversion. Coercion is implicit conversion of one type of value into another type whereas type conversion can be both implicit as well as explicit. There is a subtle difference between the both terms but most of the times, these terms are used interchangeably.

Type Coercion comes into play when you provide one type of value in a context where Javascript expected a different type of value. For example:

const result = "10" - 5;
console.log(result)       // 5

Above code example shows coercion in action. We have used a string "10" in a context where a value of type number was expected. Hence, Javascript converted "10" into a number and then performed the subtraction operation.

Let's look at some more Javascript expressions that involve coercion.

0 == false        // true
"" == false       // true
"1" == true       // true
0 == []           // true
[123] == 123      // true
{} == "[object Object]"  // true
[1] < [2]     // true

All of the above comparisons are evaluated as true. To understand the results of above expressions, we need to understand coercion and to understand coercion, we need to understand the following:

Abstract Operations

Abstract operations that deal with type conversion are algorithmic steps that need to be taken to convert any value from one data type to another. It is important to note that these abstract operations may or may not be actual functions inside the Javascript engine that can be called. They are called abstract because they are not required to be actual things. Conceptually, they are just a set of algorithmic steps that need to be taken to do type conversion.

There are many type conversion abstract operations but some of the common ones that come into play when dealing with coercion are mentioned below:

Let's understand how each of the above mentioned abstract operations works.

ToPrimitive (input[, preferredType])

ToPrimitive abstract operation is used to convert an object to a primitive value. This operation takes two arguments:

  • input: object that should be converted to a primitive value
  • preferredType: optional second argument that specifies the type that should be favoured when converting an object to a primitive value

To do the actual conversion, this operation invokes another abstract operation known as OrdinaryToPrimitive and it takes two arguments:

  • O: object that should be converted to a primitive value
  • hint: type that should be favoured when converting an object to a primitive value

ToPrimitive abstract operation invokes the OrdinaryToPrimitive abstract operation, passing in the object, that is to be converted to a primitive value, as the first argument and the second argument hint is set based on the value of preferredType argument as described below:

  • If the preferredType is a string, set hint to string
  • If the preferredType is a number, set hint to number
  • If preferredType is not specified, set hint to number

OrdinaryToPrimitive abstract operation uses the following three algorithms to convert the object to a primitive value:

  • prefer-string: If hint is "string", return a primitive value, preferring a string value, if conversion to string is possible
  • prefer-number: If hint is "number", return a primitive value, preferring a number value, if conversion to number is possible
  • no-preference: This algorithm expresses no preference about what type of primitive value should be returned and lets the objects decide what type of primitive value should be returned. If hint is "default" or if there is no hint, this algorithm is used to convert an object to a primitive value.

    It allows objects to override the default ToPrimitive behaviour. Among the built-in objects, only Date and Symbol objects override the default ToPrimitive behaviour. Date and Symbol objects implement this algorithm as prefer-string algorithm whereas all the other built-in objects implement this algorithm as prefer-number. (Objects can override the default ToPrimitive behaviour by implementing Symbol.toPrimitive method.)

The toString() and valueOf() Methods

Before going into details of how the above mentioned object-to-primitive conversion algorithms work, we need to understand the following methods:

  • toString()
  • valueOf()

Each object inherits toString() and valueOf() method and the above mentioned object-to-primitive conversion methods use these methods to perform the conversion.

toString() method, by default, converts the object into the following string representation:

const obj = { name: "Jack" };
console.log(obj.toString())    // "[object Object]"

Default representation of objects is not useful but some built-in objects override the default toString() method. For example, toString() method in Array class converts each array element into a string and joins each resulting string with commas in between. toString() method in Date class returns a human-readable string representation of the date object.

[1,2,3].toString()     // "1,2,3"
new Date().toString()  // Tue Jan 05 2021 18:49:02 GMT+0500 (PKT)

On the other hand, valueOf() method is supposed to return a primitive value that represents the object, if any such value exists. But as objects are compound values and cannot be represented by a single primitive value, so the default valueOf() method simply returns the object itself rather than returning a primitive value. Wrapper classes such as String, Number, and Boolean define valueOf() methods that simply return the wrapped primitive value.

Number(2)     // 2
String("Hello World")   // "Hello World"
Boolean(true)  // true

Arrays, functions, and regular expressions simply inherit the default method that returns the object itself on which valueOf() method was called.

const arr = [1, 2, 3]
console.log(arr.valueOf() === arr)  // true

Object-to-Primitive Conversion Algorithms

With toString() and valueOf() methods explained, we can now understand the three object-to-primitive conversion algorithms mentioned above.

prefer-string

This algorithm first calls the toString() method and if the resulting value is a primitive value, Javascript uses the returned primitive value, even if it's not a string. If the toString() method doesn't exists or it returns an object, then valueOf() method is called. If valueOf() method returns a primitive value, then that value is used, otherwise TypeError is thrown.

prefer-number

Only difference between this algorithm and prefer-string is that it first tries valueOf() method and then toString() method.

no-preference

When there is no preferred type or hint or if the preferred type is "default", by default, prefer-number algorithm is used. Objects can override this behaviour and of all the built-in objects, only Date and Symbol override this default ToPrimitive conversion behaviour. Date and Symbol use prefer-string algorithm when there is no preferred type or a hint or the preferred type is "default".

ToNumber ( argument )

ToNumber abstract operation is invoked whenever Javascript sees a non-number value in a context where it expects a number.

Some of the primitive values when converted to a number using this ToNumber abstract operation are shown below:

Screenshot 2021-01-05 at 7.45.05 PM.png

This abstract operation, when invoked with a non-primitive value as an argument, first invokes the ToPrimitive abstract operation with object as the first argument and "number" as the preferredType argument. Resulting primitive value is then converted to a number, if necessary, using the table shown in the image above.

ToString ( argument )

ToString abstract operation is invoked when Javascript sees a non-string value in a context where it expects a string.

Some of the primitive values when converted to a string using this ToString abstract operation are shown below:

Screenshot 2021-01-05 at 7.58.23 PM.png

This abstract operation, when invoked with a non-primitive value as an argument, first invokes the ToPrimitive abstract operation with object as the first argument and "string" as the preferredType argument. Resulting primitive value is then converted to a string, if necessary, using the table shown in the image above.

ToBoolean ( argument )

ToBoolean abstract operation is invoked when Javascript sees a non-boolean value in a context where it expects a boolean value.

This abstract operation is the simplest one as compared to the ones described above. Instead of using ToPrimitive abstract operation, it's just a lookup of whether the value it received as an argument, is a truthy value or a falsy value. This operation returns false for a falsy value and true for a truthy value.

Falsy values are:

  • "" (empty string)
  • 0, -0
  • NaN
  • undefined
  • null
  • false

All other values, except those mentioned above, are truthy values.

Earlier in this article, i mentioned that to understand coercion, we need to understand the following:

So far, we have seen the details of the four abstract operations. There are other operations that aren't mentioned in this article but the ones explained above are the major ones and these four operations are the most likely you will run into when dealing with coercion in Javascript.

How Abstract Equality Operator (==) Works?

Abstract Equality Operator, a.k.a double equals operator, uses number of algorithmic steps to perform the comparison between two values. Those algorithmic steps are summarized below:

  • If both operands are either null or undefined or one of them is null and the other is undefined, return true.
  • If both operands are of the same type, return the result of performing Strict Equality Comparison (===).
  • If one or both of the operands are objects, convert the non-primitive value into a primitive value, preferably into a number.
  • If both operands are primitive values but are of different data types, convert each operand into number and then return the result of their comparison.

How + Operator Works?

+ operator can be used to perform the mathematical operation of addition as well as string concatenation.

If either operand of + operator is an object, that object is converted to a primitive value using the no-preference algorithm. Once both of its operands are primitive values, it checks their types. If either operand is a string, other operand is converted to a string and then both strings are concatenated. Otherwise, each operand is converted to number and then addition is performed.

How Relational Operators Work?

Relational operators (<, >, <=, >=) can be used to compare both strings as well as numbers. If either operand is an object, it is converted into a primitive value using prefer-number algorithm. One important point to note here is that, unlike ToNumber abstract operation, if the primitive value returned by the prefer-number algorithm isn't of type number, that returned value isn't converted to a number.

For Date objects, the no-preference algorithm converts to a string value, so the fact that JavaScript uses the prefer-number algorithm for these operators means that we can use them to compare the order of two Date objects.

Understanding Coercion Examples

Now that we know the major type conversion abstract operations, how abstract equality operator (==), + operator and relational operators work, let us test our knowledge and see if we can explain the result of some code examples that involve coercion.

Lets us start with the following example:

10 + "5"

Remember how + operator works. If one of its operand is a string, it converts the other operand to string and then performs concatenation. That is why, 10 is first converted to a string, i.e. "10" and then concatenated with "5", giving us the output of "105".

Let's take a look at another example involving + operator:

[2] + 5

Can you predict the output? Evaluation of the above expression can be described in following steps:

  1. Recall that when either or both of the operands of + operator are objects, it converts that operand into a primitive value using no-preference algorithm.

  2. By default, no-preference algorithm uses prefer-number algorithm. So to convert [2] into a primitive value, prefer-number algorithm will be used.

  3. prefer-number algorithm will first call valueOf() method and if it gets a primitive value as a result, it uses that value, otherwise, toString() method is called. In case of [2], valueOf() method will simply return the array itself, so, after that, toString() method will be invoked.

  4. toString() method for arrays converts each array element into string and then joins each resulting string with commas in between. So, toString() method when called on [2], will return "2".

  5. At this point, one operand of the above expression is a string and the other is a number, so 5 is converted to a string and then "2" and "5" are concatenated together, giving us the output of "25".

Now, let us look at the example of different expressions that we saw at the start of the article:

0 == false        
"" == false       
"1" == true       
0 == []           
[123] == 123      
{} == "[object Object]"  
[1] < [2]

Can you explain how all of the above comparisons evaluate to true? Each of the above expressions are evaluated as explained below:

  • 0 == false - Earlier, when summarizing the abstract equality operator (==), i mentioned that when both operands are primitives and are of different data type, both operands are converted to number and then compared. Since, 0 is already a number, so false will be converted to a number using ToNumber abstract operation. For false value, this operation returns the value zero, so our expression 0 == false, after coercion, will become 0 == 0, which then evaluates to true.

  • "" == false - Again, as both operands are primitives, both will be converted to number using ToNumber abstract operation. Both "" and false will be converted to 0 (zero). So the expression "" == false, after coercion, will become 0 == 0 which is obviously true.

  • "1" == true - Once again, ToNumber abstract operation will be used to convert both operands to number. So the expression "1" == true, after coercion, will turn into 1 == 1 which is true.

  • 0 == [] - Now one operand is an array. When any operand of the abstract equality operator (==) is an object, it is converted to a primitive value using the ToPrimitive abstract operation. This operation will invoke OrdinaryToPrimitive abstract operation with "number" as a hint. Since hint is "number", prefer-number algorithm will be used to convert [] into a primitive value, i.e. "" (empty string), turning our expression into 0 == "". Now both operands are primitive values, so as in the earlier expressions, non-number operands will be converted to number using ToNumber abstract operation. This will turn our expression from 0 == "" into 0 == 0 because empty string, when converted to a number, is converted into zero. Finally, after all that type coercion, finally, our expression 0 == [] will become 0 == 0, which is obviously true.

  • [123] == 123 - first operand, i.e. [123] will be converted to a primitive value using the ToPrimitive abstract operation. This operation will invoke OrdinaryToPrimitive abstract operation with "number" as a hint. Since hint is "number", prefer-number algorithm will be used to convert [123] into a primitive value, i.e. "123", turning our expression into "123" == 123. At this point, "123" will be converted to a number. So our expression [123] == 123, after coercion, will become 123 == 123, which is true.

  • {} == "[object Object]" - first operand, i.e. {} will be converted to a primitive using ToPrimitive abstract operation. This operation will invoke OrdinaryToPrimitive abstract operation with "number" as a hint. Since hint is "number", prefer-number algorithm will be used to convert {} into a string, i.e. "[object Object]". So, after coercion, {} == "[object Object]" will be converted to "[object Object]" == "[object Object]", which evaluates to true.

  • [1] < [2] - As both operands are objects, both of them will be converted to primitive values using ToPrimitive abstract operation. This operation will invoke OrdinaryToPrimitive abstract operation with "number" as a hint. Since hint is "number", prefer-number algorithm will be used to convert both operands [1] and [2] into a strings, i.e. "1" and "2", respectively. So, after coercion, our expression [1] < [2] will become "1" < "2". As strings are compared using their unicode code points, "1" is less than "2", so our expression evaluates to true.

Summary

Type Coercion is an implicit conversion of one type of value into another type of value. Coercion comes into play when javascript sees a value of one type in context where it expected a value of different type.

To convert objects into primitive values, ToPrimitive abstract operation is used. This operation, in turn, invokes OrdinaryToPrimitive abstract operation which, depending on the hint argument, uses the following three algorithms to convert an object to a primitive value:

  • prefer-number
  • prefer-string
  • no-preference

To convert non-number value into a number, ToNumber abstract operation is used. This operation also uses ToPrimitive abstract operation to convert objects into a primitive value. Primitive value returned by ToPrimitive is then converted to a number.

To convert non-string value into a string, ToString abstract operation is used. This operation also uses ToPrimitive abstract operation to convert objects into a primitive value. Primitive value returned by ToPrimitive is then converted to a string.

To convert a non-boolean value into a boolean, ToBoolean abstract operation is used.

This abstract operation is the simplest one as compared to the ones described above. It is just a lookup of whether the value it received as an argument, is a truthy value or a falsy value. This operation returns false for a falsy value and true for a truthy value.

If you have read this far, i hope coercion is not as scary as it was before you read this article. If it still is, i suggest you take a break and then read this article again. It's a long article and most people will need to read this article more than once to truly understand it.

Did you find this article valuable?

Support Yousaf Khan by becoming a sponsor. Any amount is appreciated!