Serious JavaScript Fun: [1] + [1] - [1] = ?

Following tweet by Kyle intrigued me to write this post. The question was originally asked on reddit.

First, let’s try to understand what [1] + [1] will evaluate to. Questions like these are better answered by consulting ECMAScript specification. In this case, we are interested in the specification for the + operator to begin with.

(Link to the ES6 specification for addition operator.)

The first key takeaway from the specification is:

The addition operator either performs string concatenation or numeric addition.

So, it’s obvious that + operator is overloaded in the language to work with two different primitive types string and number to begin with.

1 + 2; // 3"Hello" + "World"; // HelloWorld

But then what happens if + is called with operands of type other than string and number?

[1] + [1] // WTF?

Again, specification comes to the rescue. But before we get into how + operator deals with non-primitive values, first let’s try to understand how non-primitive values are converted to primitives.

(There will be a lot of fancy spec-speak below. I have tried to simplify the terminology. But still it can be overwhelming if you are reading articles that deal with specification for the first time. But trust me, your patience will be rewarded well in the end.)

This post assumes you are familiar with primitive types and non-primitive Object type at a high level. (You can read MDN documentation if you need a quick refresher.)

var a = 20; // primitive Number type
var msg = "Hello"; // primitive String type
var account1 = {total: 100}; // non-primitive Object type
var account2 = {total: 200}; // non-primitive Object type

Sometimes our programs need to convert non-primitive values to primitive values. For example, doing account1 + account2 has to return something as it’s a valid usage of addition operator. In this case a reasonable output is 300, but that’s not what the engine returns though. It actually returns string value “[object Object][object Object]”. By the end of this post you will understand why is it so.

The specification behind converting a non-primitive to primitive is a bit elaborate, but we will try to understand this in bits by simplifying the specification terminology.

Whenever a non-primitive needs to be converted to a primitive value, ToPrimitive abstract operation is called to handle this conversion.

Specification says the following about abstract operations:

These operations are not a part of the ECMAScript language; they are defined here to solely to aid the specification of the semantics of the ECMAScript language.

Yes, that’s more than confusing, and I had to re-read it a few times. Basically, it’s a fancy term to indicate these operations are internal only operations, though ES6 gives more control on some of these operations as we are going to see below.

Let’s take an example abstract operation and understand more.

Whenever the engine needs to convert a value to its primitive value, ToPrimitive abstract operation is performed. Prior to ES6, developers did not have much direct control over this operation. Thanks to ES6, now we can override this abstract operation on any object.

Let’s first see an example and then understand the specification for ToPrimitive abstract operation.

In the above snippet:

  1. Line 1 declares a plain object customNumber.
  2. Then lines 3–5 add a new method. We are using one of the well known ES6 Symbols Symbol.toPrimitive as the key of this method. (Unfortunately, explaining Symbols takes a post in itself. But in this specific example, think of Symbol.toPrimitive as a secret key to overriding what was an internal only operation prior to ES6.)
  3. With this, whenever JavaScript engine needs to convert our customNumber object to a primitive value, it’s going to call this method and use its return value, which is 10 in this case.
  4. On line 6, using customNumber + customNumber logs 20. We are going to see more about how + operator works with non-primitives, but the point here is that our ToPrimitive custom method is definitely called.
  5. If we did not have this method, the output will be:
    “[object Object][object Object]”

Now that we know how we can add a custom ToPrimitive method on any object, let’s try to understand more on how the specification defines ToPrimitive abstract operation.

From the spec:

The abstract operation ToPrimitive takes an input argument and an optional argument PreferredType. The abstract operation ToPrimitive converts its input argument to a non-Object type. If an object is capable of converting to more than one primitive type, it may use the optional hint PreferredType to favor that type.

When ToPrimitive abstract operation is called on any primitive input, it returns the input value passed in.

When it’s called with Object type as input, it performs an elaborate algorithm as per the specification to convert the Object type to a primitive value.

If you look at the ToPrimitive algorithm in the specification, and if you are not familiar with reading the specification before, it might be difficult to understand what’s happening there. I will use a simplified version of the actual algorithm here, but you can always read the specification to find more details.

  1. If PreferredType is not passed in, set the hint to “default”. (PreferredType indicates what primitive type we are trying to convert to. hint is like a local variable used in the algorithm.)
  2. Else if PreferredType is String, then set hint to “string”.
  3. Else if PreferredType is Number, then set hint to “number”.
  4. If there is a method on the input object with the key Symbol.toPrimitive, then:
    1. Call that method and pass the hint as the first parameter.
    2. If the return value of this method is of non-Object type, then return that value. Else throw TypeError.
  5. (If we are on this step, it means there was no custom Symbol.toPrimitive method.)
    If hint is “default”, set hint to “number”. (This is an important step in the algorithm. ToPrimitive algorithm favors converting to number if the hint is not passed or if the hint is “default”.)
  6. Return the result of calling OrdinaryToPrimitive(input, hint).

(Now you can see why our customNumber + customNumber returned 20 with a Symbol.toPrimitive method. Primitive equivalent of our customNumber object is number 10.)

That takes us to the OrdinaryToPrimitive operation. Before ES6, there was no direct way to override ToPrimitive operation explicitly on objects. And the ToPrimitive algorithm was a bit different in pre-ES6 specifications, but the end result would have been same.

Following is a simplified algorithm for OrdinaryToPrimitive operation which gets invoked when there is no special Symbol.toPrimitive method on the object. (Step 6 in the above algorithm.)

  1. If hint is “string” then, let methods be a list of: “toString”, “valueOf” in that order
  2. If hint is “number” then, let methods be a list of: “valueOf”, “toString” in that order
  3. For each method in the above list of methods, do the following:
    1. Call the method on the object if it exists.
    2. If the return value is of non-Object type, then return that value.
  4. Throw TypeError (This means neither valueOf nor toString returned a primitive value. So engine can’t substitute a primitive equivalent of the object.)

Now that’s a lot of specification to digest. Let’s see a quick example to clarify this.

It’s a similar example as we have seen above using Symbol.toPrimitive.

But here we have added a valueOf method to our customNumber object. On line 7, when we try to do customNumber + customNumber, we can see that our valueOf method is called. As we are soon going to see below, + operator first calls ToPrimitive abstract operation on the operands with no PreferredType hint value. That means the hint is “default” and if there is no custom ToPrimitive method on the object, then hint ends up being “number” when OrdinaryToPrimitive is called.

As the hint is “number”, you can see from our OrdinaryToPrimitive algorithm above that it first calls valueOf method on the input. Since valueOf returns a primitive value, its return value 10 is used as the primitive value for customNumber.

(If valueOf were to return an object, toString will be called and its return value will be used. If both returned an object, a TypeError would have been thrown. It’s also possible that the object in question may not have valueOf and toString methods if the object was created with Object.create(null). Engine will throw a TypeError even in this case.)

And that’s, my friends, is how a non-primitive is converted to a primitive in JavaScript. Congratulations! Pat on your back! Take a deep breath. You have come a long way wading through the specification.

Now let’s turn our attention to the + operator.

Addition operator is overloaded in JavaScript: meaning it can work with operands of different types like numbers, strings, objects, and etc. The original algorithm in the specification is lengthy.

To simplify the algorithm in layman terms for lval + rval expression, I am skipping other calls like GetValue, ReturnIfAbrupt, and etc from the specification here. (I recommend you read through the actual specification after you read this post.)

  1. Call ToPrimitive abstract operation with no hint for input lval, call the result of it lprim
  2. Call ToPrimitive abstract operation with no hint for input rval, call the result of it rprim
  3. If lprim is a string or rprim is a string, then convert both lprim and rprim to strings using ToString abstract operation. Return the concatenated string as the result.
  4. Else, convert both lprim and rprim to number using ToNumber abstract operation. Then return the mathematical sum of those two number values.

The key here is that ToPrimitive is called without PreferredType hint in steps 1 and 2.

With this knowledge, let’s try to understand what’s the output for [1] + [1] will be.

Now we know that + operator first calls ToPrimitive abstract operation on the operands without a PreferredType hint, let’s first understand what will be the value of lprim and rprim in steps 1 & 2 in the above algorithm.

  1. lval = [1] and rval = [1].
  2. If we run our ToPrimitive algorithm, we can see that ToPrimitive([1]) ends up calling OrdinaryToPrimitive([1], “number”).
  3. OrdinaryToPrimitive will call valueOf and toString on [1] in that exact order as the hint is “number”.
  4. [1].valueOf() returns [1] which is not a primitive value. So it’s not used.
  5. [1].toString() returns “1” which is a string primitive value, so the primitive value of [1] is string “1”. (Array.prototype.toString on regular arrays like these joins the elements with “,”. If you try [1,2].toString(), you will get string “1,2”.)
  6. lprim is string “1” and rprim is string “1” as well.

Since at least one of the operands is a string, step 3 in the + operator algorithm calls ToString abstract operator on lprim and rprim and returns concatenated string, which is string “11”.

So the mystery is solved for [1] + [1] as string “11”.

It’s not that bad. See, once we know how non-primitives are converted to primitives and how + operator works, it’s easy to reason why [1] + [1] is string “11”.

That now leaves us with the remaining puzzle: “11” - [1] = ?

By now, I am sure you know where I am headed to. Yes, that’s right! Let’s consult ECMAScript specification for subtraction operator.

(Link to the ES6 specification for subtraction operator.)

I am going to spare the space here and simplify the algorithm in a sentence:

Call ToNumber abstract operation on both values and return the mathematical subtraction of those two numbers.

That again brings in a new abstract operation ToNumber. To simplify this post, we are not going to see all the details of how this abstract operation works.

Let’s see an example instead.

We will run through what happens for ToNumber([1]):

  1. ToNumber first calls ToPrimitive([1], Number).
  2. Please note the PreferredType hint is being passed as Number here. If you run the ToPrimitive algorithm for this, you will see that we end up first calling [1].valueOf() and since it returns [1], which is not a primitive, we end up calling [1].toString(), which returns string “1”.
  3. Now ToNumber calls ToNumber(“1”). This performs another algorithm to return a number equivalent of string “1”, which in this case is number 1.

Similarly, ToNumber(“11”) will be number 11.

Now we have our final equation as: 11 - 1

So the mystery is solved, and the answer for [1] + [1] - [1] is number 10.

(Hint: Don’t guess. Run through the algorithms. Also, answer might not always be a number. If needed, consult the specification. Remember TypeError?)

For answers, you know what to do right, try each expression out in the console :)

Congratulations for making it till the end. I know it’s a lot of fancy ECMAScript specification to digest. But consulting the official specification pays off well in cases like these.

Having said that, will you run into a question like this daily? I am afraid not. But understanding fundamental abstract operations like ToPrimitive, ToNumber, and others will come in handy one time or the other, especially when you are dealing with coercion.

I strongly recommend you read Types & Grammar book from YDKJS series by Kyle Simpson if you want to master types in JavaScript.

If you liked reading this post, you can show some love by clicking on 💚 :)

For my day job @Salesforce, I write code for machines; But in my free time, I pursue this wild hobby of writing for humans. {Learn | Code | Read | Write}

For my day job @Salesforce, I write code for machines; But in my free time, I pursue this wild hobby of writing for humans. {Learn | Code | Read | Write}