DEV Community

Cover image for JavaScript WTFs explained - Type coercion
Thomas C. Haflich
Thomas C. Haflich

Posted on

JavaScript WTFs explained - Type coercion

Cover Photo by Tony Tran on Unsplash

A screenshot of the Firefox developer console with several confusing results output - this will be the subject of today's discussion

Source:

The few examples to follow look pretty unhinged, fair warning.

The factor most at fault is automatic type conversion due to loose typing, but operator overload is also a problem.


The plus + operator is overloaded

The + operator in JavaScript has two different functions. As you might expect, it can add numbers together, but it can also concatenate strings.

๐Ÿ’ก Concatenation is when two (or more) strings are taped together to form a new, combined string.

For example, concatenating "hello" and "world" results in "helloworld".
This is done with the + operator in JavaScript, but other languages might use other methods, such as a concat() function or a different operator.

When an operator can do multiple different things like this, it is referred to as an overloaded operator.

By contrast, if you look at the minus - operator, it only does math, and doesn't have a defined operation for strings at all. Minus is not overloaded.


Type coercion

JavaScript has two distinct equality operators, one that compares content but not type (==), and one that compares content and type (===).

const a = "2"; // string
const b = 2; // number

a == b; // "2" == "2"
a === b; // "2" === 2
Enter fullscreen mode Exit fullscreen mode

So how does the loose equality operator (==) determine if two variables have the "same" content? It converts one or both values.

Taking a look at "2" == 2, let's go over the process step-by-step.

  1. Check if both values are of the same type. In this case, they aren't, and need to be converted.
  2. Determine the type(s) to convert to. JavaScript's loose equality algorithm determines which values to convert, and to what. In this case, the string is converted to a number.
  3. Compare the converted values. In this case, 2 (converted from string) and 2 (original) are equal, so the return value is true.

Another resource to check for this is the JavaScript Equality Table.

๐Ÿ’ก Advanced note on equality for arrays and objects

So far, we've looked at strings and numbers for == and ===, but it functions differently for more complicated constructs like arrays and objects. In these cases, both operators check that the operands have the same reference. For more information, see MDN.


Empty array + empty array

Let's take a look at the first (and easiest to explain), empty array plus empty array.

[]+[]; // evaluates to "" 
Enter fullscreen mode Exit fullscreen mode

Here's what's happening:

  1. JavaScript sees the + operator and thinks about what types are valid for this: strings and numbers. Additionally, the two operands should match each other.
    • Two strings will be concatenated ("a" + "b" => "ab")
    • Two numbers will be added numerically (1 + 2 => 3).
  2. JavaScript looks at what the actual variables used in the operation, and sees if any need to be converted. In this case, both arrays need to be converted, since they aren't strings or numbers. For +, both arguments must be converted to "primitive" values (either a string, number, or boolean).
  3. JavaScript uses a specific set of logic to cast the arguments to the type it wants
    • Arrays are converted to a predetermined primitive type, which is a string ๐Ÿ“. An empty array becomes an empty string, "".
  4. Two empty strings concatenated together make an empty string - and there's your result!

๐Ÿ“ How does an array become a string?

In JavaScript, you can test this out by using String([...]) or ([...]).toString() to observe how different arrays are transformed into strings.

The elements in the array are transformed into strings, and those stringified elements are joined together with a comma.

String(["a", "b"]); // "a,b"
String(["a", 2, ["b", 3]]); // "a,2,b,3"

This is much different from the result of JSON.stringify!


Empty array + empty object

This one works very similarly to the above. JavaScript determines that both operands need to be strings in order to continue, and converts them both. As we have seen previously, [] becomes "".

The only difference is that an empty object {}, when cast to a string, becomes "[object Object]".

๐Ÿ’ก Why "[object Object]"?

From https://stackoverflow.com/a/25419538

As others have noted, this is the default serialisation of an object. But why is it [object Object] and not just [object]?

That is because there are different types of objects in Javascript!

Object specification


Empty object + empty array

Despite the title of this section, the {} is actually not interpreted as an object.

When used at the start of an expression, and there's no other indication that it ought to be an object, JS decides it's a block statement instead.

Here's what a non-empty block statement looks like:

{
    let foo = 'bar';
    console.log(foo);
}
Enter fullscreen mode Exit fullscreen mode

You've seen these before, though they're usually used with constructs like if-statements, loops, and the like:

if (true) {
    let foo = 'bar';
    console.log(foo);
}
Enter fullscreen mode Exit fullscreen mode

In the case of this particular example, it's interpreted as an empty block statement, which is functionally equivalent to typing nothing.

The reason the previous example was interpreted as an empty object instead of an empty block was that it was being used as a right-hand operand for +. JavaScript supports + being used to cast to number type when used without a left-hand operand:

// left and right
1 + "1"; // "11"

// casting with right-side only
+"1"; // 1
+1; // 1
+true; // 1
Enter fullscreen mode Exit fullscreen mode

Thus, we can reduce the problem to +[].

When JS converts an array to a number, it is effectively the same as casting that same array to a string (as seen in the two previous examples), then casting that string to a number. (Note that casting the array to a string converts values such as null and undefined to empty string as well.)

[].toString(); // ""
Number(""); // 0

["1"].toString(); // "1"
Number("1"); // 1

["1", "2"].toString(); // "1,2"
Number("1,2"); // NaN, can't parse this

["a"].toString(); // "a"
Number("a"); // NaN, can't parse this
Enter fullscreen mode Exit fullscreen mode

So now we can see where the 0 comes from - it's the numeric value of an empty string.


Type coercion with different operators

So far we've worked only with the + operator, which is an overloaded operator: it can be used for more than one thing, depending on the operands. However, the mathematical opposite, -, is only used for mathematical subtraction, and can't be used to manipulate strings at all.

The examples used in the reddit post:

9 + "1"; // "91"
91 - "1"; // 90
Enter fullscreen mode Exit fullscreen mode

Since one of the operands of 9 + "1" is a string, even if it's a numeric string, the + operation results in a string concatenation.

However, since - cannot be used for string concatenation, the "1" is interpreted as numeric.

Here are some more examples of this principle in action:

// two numeric operands is always math
1 + 2; // 3
1 - 2; // -1

// numeric strings are concatenated for addition, and converted for subtraction
1 + "2"; // "12"
"1" + 2; // "12"
"1" + "2"; // "12"
1 - "2"; // -1
"1" - 2; // -1
"1" - "2"; // -1

// non-numeric strings are concatenated for addition, and result in a math error for subtraction
1 + "hello"; // "1hello"
"hello" + 1; // "hello1";
1 - "hello"; // NaN
"hello" - 1; // NaN

Enter fullscreen mode Exit fullscreen mode

When is a true a 1?

true + true + true === 3; // true
true - true; // 0
Enter fullscreen mode Exit fullscreen mode

As in the section dealing with []+[], we know that JavaScript will convert operands if they aren't of the appropriate type. In this case, the boolean value true is cast to a numeric 1, and as we all know, 1 + 1 + 1 is in fact three.

Similarly, 1 - 1 we know to be zero.

Note that these can be cast to the strings "true" and "false", for example when a string is an operand:

1 + "hello" + true + false; // "1hellotruefalse" 
Enter fullscreen mode Exit fullscreen mode

When is a true not a 1?

true == 1; // true
true === 1; // false
Enter fullscreen mode Exit fullscreen mode

The numeric value of true is 1, so the loose equality operator sees them as having the same value.

The reddit post lists the following statements:

true == 1; // yes, because true is cast to 1
true === 1; // no, because true and 1 are not the same type
[] == 0; // yes, because empty array is cast to 0 (see empty object + empty array example)
Enter fullscreen mode Exit fullscreen mode

Abusing type coercion for fun and profit

The last example from reddit is basically a gigantic exercise in learning automatic type conversion, so let's break it down.

(!+[]+[]+![]).length; // 9
Enter fullscreen mode Exit fullscreen mode

The first thing we need to ask here is: The length of what, exactly? Let's take a look at what's in those parentheses.

!+[]+[]+![]; // "truefalse"
Enter fullscreen mode Exit fullscreen mode

The .length might have led to you believe that it was an array of length 9, but here we have a string of length 9. Now, let's figure out why we get that string out of that expression.

Let's separate it out a little bit, so it's more readable:

(!+[]) + ([]) + (![]);
Enter fullscreen mode Exit fullscreen mode

That first set of parentheses contains two explicit conversions, which are worked from the "inside out" - the closest conversion to the value goes first, then it works its way outwards. So first, [] is our value, which is converted to a number by +[], which results in 0. Next, the ! converts the number to a boolean value and inverts it - 0 converts to a boolean false, which when inverted results in true.

Let's leave that empty array in the middle for a moment. It isn't being explicitly cast.

On the right, we have ![] - at first glance, it seems like this also ought to be true, since !+[] is true. However, the extra conversion to number makes all the difference! When converted directly to a boolean value, an empty array results in true, which when inverted is false.

Number([]); // 0
Boolean([]); // true
Boolean(0); // false
Enter fullscreen mode Exit fullscreen mode

So now, we have the simplified form of our expression from earlier:

true + [] + false
Enter fullscreen mode Exit fullscreen mode

Now on to that + operand. The choice with + is either numeric plus, or string concatenation, and JavaScript's internal ruleset says that when you are using + with a boolean and an array, both are cast automatically to strings and concatenated. From left to right, we can work this expression out:

// true + [] + false
true + []; // "true"
"true" + false; // "truefalse"
Enter fullscreen mode Exit fullscreen mode

And there you have it.


Wait, so... why?

Why is JavaScript loosely typed, and why does it do automatic type conversion?

Brandon Eich designed the language in 1995 in under two weeks. I don't really think there's a "why" to it, to be honest. Maybe we can blame a lack of sleep on his part.

Why does X convert to Y?

There is an internal logic to all the automatic type conversion JavaScript does - it is, after all, run by a computer, and computers don't just spontaneously decide to do something a little different to mess with you.

You might notice that arrays and objects are tricky, being cast to many different possible primitive values:

// empty arrays
Number([]); // 0
String([]); // ""
Boolean([]); // true

// arrays with stuff in them
Number(["a"]); // NaN
Number([1]); // 1
Number([2]); // 2
Number([1, 2]); // NaN
Number([null]); // 0
Number([true]); // NaN
String(["a"]); // "a"
String(["a", 1]); // "a,1"
String([null]); // ""
Boolean([false]); // true

// empty objects
Number({}); // NaN
String({}); // "[object Object]"
Boolean({}); // true
Enter fullscreen mode Exit fullscreen mode

Most JavaScript-specific WTFery actually depends, deep down, on these conversions.

Final notes

Much of this isn't JavaScript-specific. The many examples of JavaScript shenanigans are often simple floating-point errors, which can be found in any programming language that uses floating point numbers. (Please stop using these as examples of why JavaScript is bad. There's enough wrong about JavaScript to be mad about without bringing in floating point arithmetic.)

The true WTF is not necessarily that JavaScript can do automatic type conversion, but the absolute bonkers scale of how much it will convert, and to what.

What do I even do about this?

If you're coming from a strongly-typed language and/or felt like crying several times during this article, I suggest TypeScript.

If you can't or don't want to use TypeScript, make sure to follow these two rules:

  1. Always use strict equality (===)
  2. Always explicitly convert types, rather than allowing JavaScript to implicitly convert them for you

JavaScript can't break your stuff with automatic type conversion if you've made sure things are the right type to begin with.


You made it to the end! Congrats for wading knee-deep into JavaScript's type system. Have a picture of a squirrel eating at a tiny table for your trouble. (via reddit)

A squirrel eating at a table, there's a tiny bucket and some corn.

The other half of the Reddit post is discussed here.

Top comments (1)

Collapse
 
revenity profile image
Revenity