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 related to Type Conversions
- Abstract Equality Operator (==)
+
operator- Relational Operators (
<
,>
,<=
,>=
)
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:
- ToPrimitive (input[, preferredType])
- ToNumber ( argument )
- ToString ( argument )
- ToBoolean ( argument )
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 valuepreferredType
: 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 valuehint
: 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, sethint
to string - If the
preferredType
is a number, sethint
to number - If
preferredType
is not specified, sethint
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 nohint
, 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, onlyDate
andSymbol
objects override the defaultToPrimitive
behaviour.Date
andSymbol
objects implement this algorithm asprefer-string
algorithm whereas all the other built-in objects implement this algorithm asprefer-number
. (Objects can override the defaultToPrimitive
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:
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:
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:
- Abstract Operations related to Type Conversions
- Abstract Equality Operator (==)
+
operator- Relational Operators (
<
,>
,<=
,>=
)
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
orundefined
or one of them isnull
and the other isundefined
, returntrue
. - 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:
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.By default, no-preference algorithm uses prefer-number algorithm. So to convert
[2]
into a primitive value, prefer-number algorithm will be used.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.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".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, sofalse
will be converted to a number using ToNumber abstract operation. Forfalse
value, this operation returns the value zero, so our expression0 == false
, after coercion, will become0 == 0
, which then evaluates to true."" == false
- Again, as both operands are primitives, both will be converted to number using ToNumber abstract operation. Both""
andfalse
will be converted to 0 (zero). So the expression"" == false
, after coercion, will become0 == 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 into1 == 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 ahint
. Sincehint
is "number", prefer-number algorithm will be used to convert[]
into a primitive value, i.e.""
(empty string), turning our expression into0 == ""
. 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 from0 == ""
into0 == 0
because empty string, when converted to a number, is converted into zero. Finally, after all that type coercion, finally, our expression0 == []
will become0 == 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 ahint
. Sincehint
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 become123 == 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 ahint
. Sincehint
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 ahint
. Sincehint
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.