JavaScript lets you mix and match types quite happily, and in most cases the result might be exactly what you expect:
> 'Total : ' + 100'Total : 100'> '12' * 336
However you might not get what you expect:
> '15' + 1'151'> '15' - 114
> if([]) {... console.log('hello')... }helloundefined> [] == truefalse
Info
Remember ==
here is the equality operator which attempts to convert and compare operands, whereas ===
is strict equality and does not do any conversions.
#
How and why Type Coercion occursThis automatic conversion of types from one type to another is the result of Type Coercion. And it happens when you use operator on some operand(s) which expect a different type than the one(s) you've given. In JavaScript there are two main groups of types: primitive types and Objects.
#
Primitive TypesThe primitive types are quite straightforward:
- string
- number
- bigint
- boolean
- symbol
- null
- undefined
If we look at the addition operator +
spec, you can see that if either argument is a string, the other argument will also be converted to a string and then concatenated, otherwise numerical addition is performed. So String concatenation takes precedence, and for the subtraction operator -
, subtracting strings isn't well defined so each operand is converted to a number.
> undefined + 0NaN> null + 00> true + 01> false + 00> '' + 0'0'
Info
You can explicitly convert types to test the conversion between primitive types, e.g. Number(true)
is 1
.
#
Object typesSo then the question is, if the addition operator is well defined for string
and number
primitives, what happens when you pass in Object types such as []
and {}
? Well the operator works on primitive types, so the Object has to be converted to a primitive. And an Object may have more than one primitive representation, consider the Date Object as an example:
> const now = new Date();undefined> now.toString()'Sun Oct 03 2021 15:57:16 GMT+0100 (British Summer Time)'> now.valueOf()1633273036802
There are three fundamental algorithms for converting objects to primitive values:
- prefer-string (Return a primitive, string if possible)
- prefer-number (Return a primitive, number if possible)
- no-preference (JavaScript built-in types all implement this as prefer-number except Date which uses prefer-string).
Objects will have both valueOf
and toString
methods. If the prefer-string algorithm is needed, toString
will be called. If the result is a primitive which is a non-string, it will be converted to a string. Now if toString
doesn't exist or if toString
returns an object then valueOf
will be called instead. And if that returns an object you'll encounter a TypeError otherwise the primitive returned is converted to a string.
The prefer-number algorithm will work in a similar way, but will try valueOf
over toString
to begin wtih.
And the no-preference algorithm for built in JavaScript types will use valueOf
over toString
apart from the Date
class.
Now the addition +
operator works for both string
and number
types, and the no-preference rule will be used. ==
also uses the no-preference algorithm.
So what will happen if you do now + 1
which is a Date + number
? The no-preference rule will be used and in this case, the Date will be converted to a string and concatenated with 1.
> now + 1'Sun Oct 03 2021 15:57:16 GMT+0100 (British Summer Time)1'
And if you do now - 1
, the prefer-number algorithm will be used, converting the Date to a number and subtracting 1 from that:
> now - 11633273036801
And now if I gave you the following information:
> [1,2,3][ 1, 2, 3 ]> [1,2,3].valueOf()[ 1, 2, 3 ]> [1,2,3].toString()'1,2,3'
Could you guess what [1,2,3] + 1
would give? If you guessed '1,2,31'
you'd have been correct, seeing as [1,2,3]
is not a primitive, '1,2,3'
would be the primitive for [1,2,3]
and adding that to 1 would be string concatenation.
> {}.valueOf(){}> {}.toString()'[object Object]'> [].valueOf()[]> [].toString()''
And what if we add 0
to {}
and []
? We get string concatenation.
> {} + 0'[object Object]0'> [] + 0'0'
Again the +
operator is using the no-preference rule, but if we use the prefer-number rule such as with subtraction, you might see how an empty array equals zero, and an empty object is NaN
:
> 5 - []5> Number('')0> 5 - {}NaN> Number('[object Object]')NaN
So far we have dealt with numbers and strings, how does Object to priitive conversion work for other primitive types?
#
Object-To-BooleanAll Objects convert to true
e.g. !!{}
equals true
. However the rules for the ==
equality with type conversions can be tricky, for example:
> [] == truefalse
And the reason why this is false, is because when using ==
, if either value is the primitive true
it is converted to 1
and the comparison is made again:
> [0] == truefalse> [1] == truetrue> [2] == truefalse
Tip
It can be useful to checkout the documentation for the abstract equality algorithm. For example is shows that NaN == NaN
equals false
.
#
SummaryIn summary, hopefully this clarifies some of the mysteries behind JavaScript type coercion and highlights the importance of checking the ECMAScript specification. And when you do begin to understand type coercion, some of JavaScript's little mysteries start to make sense.
For an additional resource see:
- JavaScript the Definitive Guide - David Flanagan
#
Bonus QuestionIf I use console.log
on a Date
, in Node
I get neither it's toString
nor it's valueOf
result but instead something else. So what is going on here?
> console.log(now)2021-10-03T14:57:16.802Zundefined> now.toString()'Sun Oct 03 2021 15:57:16 GMT+0100 (British Summer Time)'> now.valueOf()1633273036802
Now if we have a look at the node documentation, we see that any argument to console.log
is passed into util.format, and since no format specifier is given, non strings are formatted using util.inspect. And util.inspect
returns a string
representation of an object that is intended for debugging. And now since you can browse the Node source on Github, we can see what exactly util.inspect
is doing here. And if you trace it through some more you see the call to function formatRaw(ctx, value, recurseTimes, typedArray)
.
And in that function you have:
//... } else if (isDate(value)) { // Make dates with properties first say the date base = NumberIsNaN(DatePrototypeGetTime(value)) ? DatePrototypeToString(value) : DatePrototypeToISOString(value);//...
So we can see that the Date
get's formatted by it's toISOString
method and we can check that by calling now.toISOString()
:
> console.log(now)2021-10-03T14:57:16.802Zundefined> now.toISOString()'2021-10-03T14:57:16.802Z'