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); // 1However, 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)undefinednull
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; // falseRecall 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; // trueThere is one exception to these rules: the numeric value NaN is strictly not
equal to itself, as mentioned in the previous chapter.
NaN === NaN; // falseAdditionally, 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; // trueObject.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); // trueLoose 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'; // trueBooleans 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; // trueComparing 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'; // trueThis 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 == 1For 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
TypeErrorexception
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; // trueBy default, plain objects in JavaScript have a .toString() method which
returns the string "[object Object]".
let someObj = { a: 42, b: 33 };
someObj == '[object Object]'; // trueArray objects, by default, have a different .toString() method.
[] == ''; // true
[1] == '1'; // true
[1, 2] == '1,2'; // true
[1, 2, 3] == '1,2,3'; // truenull 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; // falseAltogether, 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/
↩