Rajat Saxena
July 8, 2023
Type conversion (also known as coercion) in JavaScript confuses a lot of developers and makes a significant chunk of interview questions and general trivia/gotchas. In this post, we will learn the basic building blocks to coercion and see how to apply those to actual real world problems.
Type coercion is JavaScript's attempt to be fault-tolerant and forgiving. It will try to convert the data types of the variables you are performing an operation upon so that the operation does not error out.
Let's establish the first building block.
JavaScript will apply a few rule and ultimately all the operands will be converted to either of these values.
Glad that you asked! There are certain operations which prefer numbers and others which prefer strings. Let's see a few examples.
1.1.1. Operations that prefer numbers
* // multiply
% // modulo
++ // prefix (or postfix) operators
Listing 1
1.1.2. Operations thats prefer strings
`${}` // string templates
String() // creating an string object
Listing 2
Then there are operations which are neutral like, addition
and loose equality
(==). We will learn more about those in Section 3.
So how does JavaScript convert any value to number/string? For that, we are going to establish the second building block.
JavaScript uses a function called ToPrimitive which converts any input to a primitive value. If the input is already a primitive, it is returned as is. Otherwise, it is first converted into a primitive value and then returned.
JavaScript has the following primitives.
string
number
bigint
boolean
undefined
symbol
null
JavaScript calls ToPrimitive function automatically when the type conversion happens. While calling this function, JavaScript also sends the preference i.e. whether it would like to convert that value into a string or a number, as an input. This input is called hint
and its value can be string
| number|default
.
Following is the over-simplified pseudo-code of ToPrimitive
. Please keep in mind I have glossed over non-essential stuff and we will follow the same pattern in all of such examples.
ToPrimitive (input, hint: string|number|default = default):
if input is an object:
if input has a function called toPrimitive:
result = toPrimitive(input, hint)
if result is not an object:
return result
else:
throw error
if hint == default:
hint = number
return OrdinaryToPrimitive(input, hint)
// already a primitive
return input
Listing 3
Here is what the above pseudo-code is doing.
If the hint is not defined then it is initialised to default
.
If the input is an object, it will follow Step #3 and beyond. Otherwise, it will return the input as it is already a primitive.
If the input has a method called toPrimitive, it will call that method with the hint.
If the result of Step #3 is an not an object, it will return this result. Otherwise, it will throw an error (TypeError in particular)
If the input does not has a method called toPrimitive, first the hint is set to number and then a function call is made to OrdinaryToPrimitive.
In most cases, this toPrimitive
function is not defined hence the responsibility lands on OrdinaryToPrimitive
function to convert an object into a primitive.
Following is the implementation in over-simplified pseudo-code.
OrdinaryToPrimitive(hint: string|number):
if hint == "string"
methods = ["toString", "valueOf"]
else
methods = ["valueOf", "toString"]
loop over methods array:
if method is callable:
result = method()
if result is not of object type:
return result
else
throw error
Listing 4
So as you can see from above, JavaScript will call two methods i.e. toString
and valueOf
in a preferred sequence depending upon the hint.
We saw that ToPrimitive will return primitive values. These primitive values will be ultimately converted to string/number. JavaScript uses ToNumber and ToString for this. These methods define how values like null, undefined, symbol, bigint etc. are converted to numbers and strings respectively.
Let's see the above concepts in action. For that let's define a variable as follows.
let b = {
valueOf: function () { return 1 },
toString: function () { return 'bee' }
}
Listing 5
Now, if we do the following
b * 2 // prints 2
Listing 6
This is because the multiplication operator prefers the operands to be numbers. Hence JavaScript tried to convert the object b
to a number. For this, it invokes ToPrimitive
with number
hint, which in turn calls OrdinaryToPrimitive
with number
hint and hence valueOf
is invoked.
Let's try something else.
`${b}` // prints "bee"
Listing 7
Again, using the above logic and section 1.1.2, toString
gets invoked and hence the result.
There are operators which are neutral i.e. to don't prefer string over numbers and vice-versa. The following are a few such operators.
Addition (+)
Loose equality (==)
Less than (<)
This class of operators leads to the most confusion around coercion in JavaScript. We will see how addition and loose equality works in this post. Less than is somewhat more complex than the other two so we will leave that out for now.
Again, let's look at the pseudo-code of how this is implemented.
Addition(x, y):
convert operands to primitives
if any operand is string:
convert both operands to string
concatenate the operands
return result
else:
convert all operands to numbers
if any operand is unconvertible (symbol, bigint):
throw error
else
add both operands
return result
Listing 8
This one is simple.
If any operand is a string, convert both to string, concatenate both and return the result.
Otherwise, convert both to numbers.
If there is an error in converting to numbers, throw error. Otherwise, add both and return the result.
3.1.1. Hands-on
Let's the try the easier ones first.
'e' + 1 // prints 'e1'
1 + 2 // prints 3
Listing 9
Now, let's try a complex one.
let b = {
valueOf: function () { return 1 },
toString: function () { return 'bee' }
}
b + 1 // prints 2
Listing 10
Here, one operand i.e. b
is an object and the other one is a number. JS will try to convert both of these to primitive (see Listing 8, line 2). To convert b
to primitive, ToPrimitive
will be invoked with the default
hint. We know the if the hint is default
, it is overridden with number
(see Listing 3, line 10). Hence b will be coerced to a number. So, valueOf
is invoked which returned 1.
Now, let's try adding b
to a string
b + '1' // prints "11"
Listing 11
Since '1' is a string, hence b
will be coerced to a string (see Listing 8, lines 3-4). Hence, toString
will be invoked from b
.
One more
true + false // prints 1
Listing 12
Here, both of the operands will be converted to numbers using ToNumber (see Listing 8, line 8) and hence the result.
Brace yourself, this operation's pseudo-code is considerably trickier. The values x'
and y'
denote the values after the conversion.
LooseEquality(x, y):
If both operands are of same types, do strict comparison (===)
if comparing null and undefined:
return true
if comparing number and string:
convert the string operand to number
LooseEquality(x', y')
if comparing bigint and string:
convert the string operand to bigint
if the above results in NaN:
return false
LooseEquality(x', y')
if any operand is boolean:
convert boolean operands to number
LooseEquality(x', y')
if one operand is object and the other is one of [string, number, bigint, symbol]:
convert the object to primitive value
LooseEquality(x', y')
if comparing number and bigint:
if any operand is non-finite i.e. NaN, +Infinity, -Infinity:
return false
if the mathematical values of both operands are the same:
return true
else:
return false
return false
Listing 13
Here are some easily deduced take-aways from the above algorithm.
If the types are same, Strict equality operator (===) is used.
Comparing null to undefined will result in true
.
While comparing a string to a number, the string operand is converted to a number.
Boolean values are converted to numbers.
Objects are converted to primitive values.
3.2.1. Hands-on
3.2.1.1. Let's try this.
1 == '1' // prints true
Listing 14
The string operand '1'
gets converted to 1
(see Listing 13, line 8) and the equality operator is invoked again with arguments (1, 1). Due to Listing 13, line 2
, we get true
.
3.2.1.2.
let d = {
valueOf: function () { return 0 },
toString: function () { return 'bee' }
}
d == 0 // prints true
Here, d
will be converted to a primitive value (see Listing 13, line 21-22). ToPrimitive
will be called with the default
hint which means that the it will be coerced to a number (see Listing 3, line 10). Hence, we get 0 == 0
which is true
.
3.2.1.3.
[] == 0 // prints true
[] == false // prints true
Arrays are objects
so []
gets converted to 0
(Listing 13, line 21-22). Hence the results.
That's it! If you have questions, come discuss this over Twitter or in my Discord server.