Fundamentals of Web Application Development · DRAFTFreeman

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, and NaN
  • "" (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:

  1. If the object has a .valueOf() method which returns a primitive value, return that value
  2. Else, if the method .toString() exists and returns a primitive value, return that value
  3. 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

js equality table
JavaScript Equality Table

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 ===.