Type Coercion
Now that we know a bit about data types in JavaScript, how do we go about using them as part of our application logic? In many of the examples we’ve seen so far, you may have noticed that JavaScript seems to be converting values from one type to another “behind the scenes,” sometimes in subtle ways. A more obvious place where this occurs is in comparison statements.
42 == "42"; // true
0 == false; // true
null == false; // false <-- !?
{} == {}; // false <-- !?
Apparently, the JavaScript engine is doing some work internally to compare these values “across” their data types. Sometimes the result is intuitive, other times it is… less so. This behavior, which can sometimes appear arbitrary, is a common source of confusion (and hence software faults), and is one of the reasons that JavaScript has historically been criticized by developers who are accustomed to working with statically-typed languages. We will explore this behavior and attempt to gain an understanding of it that allows us to use it to our advantage.
Coercion is the JavaScript engine’s conversion of values into analogous representations in different data types. There are mechanisms we can use to explicitly and imperatively cause values to be coerced, such as using the built-in type “wrapper” objects.
String(42); // "42"
Boolean(42); // true
Number(true); // 1
However, most of the time in JavaScript you will encounter implicit coercion in which the engine performs these conversions somewhat “behind the scenes”.
12 + 3; // 15
12 + '3'; // "123"
12 + +'3'; // 15
+ Operator
The +
operator is overloaded in JavaScript, which means it performs different
operations depending on its operands.
In its binary form, the +
operator takes two operands. If either of the
operands is a string, it will perform string concatenation. Otherwise, it will
perform numeric addition.
'a' + 'b'; // "ab"
1 + 2; // 3 -- numeric addition
1 + '2'; // "12"
// number 1 is coerced into string "1"
The associativity of +
is left-to-right, which means that if multiple
additions (and/or concatenations) exist side by side, those on the left will be
evaluated first before continuing right. If all of the operands are of the same
type, their order does not matter – numbers behave as we would expect from
traditional mathematical notation, as numeric addition is associative, and
strings will be concatenated in the same order in which they are given. However,
if the operands are not of the same type, the order in which they appear is
significant.
1 + 2 + 3; // 6
// evaluated as (1 + 2) + 3;
1 + 2 + '3'; // "33"
// (1 + 2) + "3"
// 3 + "3"
'1' + 2 + 3; // "123"
// ("1" + 2) + 3
// "12" + 3
2 + 2 + 2; // 6
2 + 2 + '2'; // "42"
'2' + 2 + 2; // "222"
0 + 0 + 0 + 0 + 0; // 0
0 + 0 + 0 + 0 + '0'; // "00"
'0' + 0 + 0 + 0 + 0; // "00000"
Truthiness
All values in JavaScript have an intrinsic characteristic called truthiness
which determines how a value will be evaluated in a Boolean context, such as in
the argument expression of an if()
statement. Values which coerce to the
boolean primitive true
are called truthy; all other values (which,
necessarily, coerce to false
) are called falsy.
There are exactly seven falsy values in JavaScript:
false
+0
,-0
, andNaN
""
(the empty string primitive)undefined
null
That’s it. Everything else is truthy, including “empty” objects, the string
"false"
, and instances of Boolean
object wrappers (e.g., created with
new Boolean(false)
).
Comparison & Equality
JavaScript has two kinds of equality comparisons: loose and strict. Loose comparison allows for coercion between primitive types, while strict does not.
Strict Equality
Strict equality, which uses the ===
(“triple-equals”) operator, compares two
operand values and produces true
if both operands are of the same type and
are the same value. Otherwise, it produces false
.
42 === 42; // true
42 === '42'; // false
true === 1; // false
false === 0; // false
Recall that objects are what we sometimes refer to as reference types – that is, the “value” of an object is some unique identifier (I like to think of it as an object’s address in memory, though that may be an oversimplification). When we compare two objects, they are only considered equal if they are the exact same object. Even if two different objects have the exact same properties and property values, they will still be considered unequal.
{} === {}; // false
{a: 42} === {a: 42}; // false
let obj1 = {a: 42};
let obj2 = obj1; // value copied to obj2 is the "address of" obj1
obj1 === obj2; // true
There is one exception to these rules: the numeric value NaN
is strictly not
equal to itself, as mentioned in the previous chapter.
NaN === NaN; // false
Additionally, strict equality treats +0
and -0
as the same value.
Mathematically this makes sense, but the two values still have distinct
underlying bit representations according to IEEE-754.
0 === -0; // true
Object.is()
The global function Object.is()
es2015
takes two arguments,
returning true
if both are the same value and false
otherwise. It acts like
strict equality comparison, but distinguishes between +0
and -0
, and
considers NaN
equal to itself.
Object.is(0, -0); // false
Object.is(NaN, NaN); // true
Loose Equality: Comparison with Coercion
Loose or abstract equality comparison uses the ==
(“double-equals”)
operator. Often the operation is described simply as “equality checking without
types,” but the imprecision of that definition hides its actual mechanics,
leading to the confusion and frustration of many who do not understand it.
Loose comparison is more precisely described as comparison with type coercion. If two values of the same type are compared, they follow the strict comparison as mentioned above. If they have different types, the JavaScript engine will follow a specific series of steps to try and convert the values to the same type.
Numbers vs. Strings
When a string
and a number
are loosely compared, the string
value is
coerced into a number
.
42 == '42'; // true
42 == '42.0'; // true
42 == '0x2A'; // true
Booleans vs. Non-Booleans
When a boolean
is loosely compared with a non-boolean
primitive, the
boolean
is coerced into a number
(either 1
or 0
) and then compared to
the other value.
true == 1; // true
false == 0; // true
Comparing a boolean
to a string
then results in a comparison of the number
0
or 1
to said string
, which itself is also coerced into a number
according to the previous scenario.
true == '1.000'; // true
true == '0x001'; // true
false == '0e0'; // true
This behavior can lead to some unintuitive results if we do not specifically
think about coercion steps when performing the comparison. For example, we know
that the string value "42"
is truthy, so we might expect "42" == true
to
return true
. However, following the above rule for boolean
coercion in loose
comparison, this turns out to be incorrect. (Interestingly, "42"
is neither
loosely equal to true
nor false
.)
'42' == true; // false
// -> coerced to "42" == 1
// -> then to 42 == 1
'42' == false; // false
// -> coerced to "42" == 0
// -> then to 42 == 0
// Similarly
'true' == true; // false
// -> coerced to "true" == 1
// -> then to NaN == 1
For the same reasons, x == true
and Boolean(x)
are not necessarily the
same logical operation; it is possible for one to result in true
and the other
false
. Along the same lines, consider the following case:
let a = 42;
if (a) {
// code in this block is executed,
// because 42 is truthy
}
if (a == true) {
// code in this block is NOT executed,
// because (42 == true) is false
}
Because of this non-intuitive behavior, you probably want to avoid loose
equality comparisons to boolean
values if you can find another way to express
your logic in code that is easier to reason about.
Objects vs. Primitives
Loose comparison of objects to primitive values is possible, but has even
trickier behavior. The ECMAScript spec defines an abstract operation
ToPrimitive
which is used internally by the JavaScript engine to coerce an
object to a primitive value. It roughly follows the below steps to try and get a
primitive (non-object) value from an object to use for comparison:
- If the object has a
.valueOf()
method which returns a primitive value, return that value - Else, if the method
.toString()
exists and returns a primitive value, return that value - Else, throw a
TypeError
exception
For example, instances of the built-in primitive wrapper objects Number
,
String
, and Boolean
have a .valueOf()
method which returns the wrapped
primitive value.
let numberWrapper = new Number(42);
typeof numberWrapper; // "object"
numberWrapper.valueOf(); // 42
numberWrapper == 42; // true
By default, plain objects in JavaScript have a .toString()
method which
returns the string "[object Object]"
.
let someObj = { a: 42, b: 33 };
someObj == '[object Object]'; // true
Array objects, by default, have a different .toString()
method.
[] == ''; // true
[1] == '1'; // true
[1, 2] == '1,2'; // true
[1, 2, 3] == '1,2,3'; // true
null
vs. undefined
The values null
and undefined
are considered loosely equal to each other
only. Luckily, this is fairly easy to remember.
undefined == null; // true
null == undefined; // true
undefined == undefined; // true
null == null; // true
0 == null; // false
'' == undefined; // false
Altogether, a standard JavaScript equality table appears as thus:1
The loose equality table for JavaScript has been called a “minefield” as it can be all too easy for developers to accidentally land on a square which gives behavior that they don’t anticipate.
In general, your application logic should avoid relying upon some of these more esoteric behaviors, such as loose equality comparison between an object and a non-object. If you find these in use in your own programs, it’s probably worth considering whether that part of the logic could be re-written in such a way that its behavior will be more obvious to future maintainers (including yourself!).
A good rule of thumb: when in doubt, use ===
.
-
Adapted from dorey.github.io/JavaScript-Equality-Table/unified/
↩