Introduction
console.log(1 + {}) //"What is the result of the following expression?"
If you've ever seen one of those trick questions and felt lost, you're not alone. I wanted to develop a set of tips, grounded in the ECMAScript spec, to help me predict the outcome of common additive expressions like these. My research led me to write this blog post, so hopefully after reading this, you can answer the question above and others like it.
When faced with an expression like the one presented above, it helps to split the task into two steps:
- Predicting the type, and
- Predicting the value.
Since predicting the type is more than half the battle, this blog post will focus primarily on step 1 and while covering some common coercions to help with step 2.
Disclaimer
This blog post will only deal with additive expressions with two unmodified operands. Since most of the expressions in these interview-style questions contain mixed types, those will be the focus of this article. For example 1 + "a" as opposed to 1 + 2.
This blog post will not cover all possible combinations and exceptional cases.
The String Gate
The ECMAScript spec extensively outlines how such additive expressions are evaluated, but you can get a solid understanding by analyzing the function ApplyStringOrNumericBinaryOperator.
ApplyStringOrNumericBinaryOperator ( lVal, opText, rVal )
In the expression 1 + {}, lVal would be 1, opText would be + and rVal would be {}. Here's the beginning of the algorithm:
1. If opText is +, then
a. Let `lPrim` be ? `ToPrimitive(lVal)`.
b. Let `rPrim` be ? `ToPrimitive(rVal)`.
c. If `lPrim` is a String or `rPrim` is a String, then
i. Let `lStr` be ? `ToString(lPrim)`.
ii. Let `rStr` be ? `ToString(rPrim)`.
iii. Return the string-concatenation of `lStr` and `rStr`.
It might feel like a lot to digest, but I want to highlight step 1c., which I dub the 'String Gate'.
c. If `lPrim` is a String or `rPrim` is a String,
then ... Return the string-concatenation of `lStr` and `rStr`.
ToPrimitive is a function that returns primitives as-is and attempts to coerce objects to primitives. So, this brings us to the first tip:
If an additive expression contains a string, it will evaluate to a string.
Here are some examples:
// At least one operand is a string
console.log("1" + 2); // "12"
console.log("1" + false); // "1false"
console.log("1" + {}); // "1[object Object]"
We can predict what operands will be coerced into when an expression contains a string by looking at what ToString does. For our purposes, here are the conversions:
| Data Type | Outcome |
|---|---|
| String | Returns as-is |
| Symbol | Throws a TypeError |
| undefined | Returns 'undefined' |
| null | Returns 'null' |
| true | Returns 'true' |
| false | Returns 'false' |
| Number | Returns Number::toString(argument, 10) |
| BigInt | Returns BigInt::toString(argument, 10) |
| Object | Returns ToPrimitive(argument, STRING)** |
** We'll touch on ToPrimitive in a bit.
console.log("John " + 1n); // "John 1"
console.log("John " + undefined); // "John undefined"
Strings are caught by the 'String Gate'.
Dates
To prepare for the next tip, let's start with this note for the ApplyStringOrNumericBinaryOperator function:
1. If `opText` is +, then
a. Let `lPrim` be ? `ToPrimitive(lVal)`.
b. Let `rPrim` be ? `ToPrimitive(rVal)`.
...
Note 1
**No hint is provided in the calls to `ToPrimitive` in steps 1.a and 1.b.**
All standard objects except Dates handle the absence of a hint
as if NUMBER were given;
Dates handle the absence of a hint as if STRING were given
To make sense of this note, we need to review the function at the heart of ToPrimitive, which is OrdinaryToPrimitive.
OrdinaryToPrimitive ( obj, hint )
In this function, there are two method options: "toString" and "valueOf". The order of execution will depend on the value of hint. If hint is "string", then "toString" will be called first. If not, then "valueOf" will be called first.
If the first method's result is a primitive, then that value will be returned from OrdinaryToPrimitive. If not, then the second method is called, and its value is checked as well. If neither returns a primitive, then a TypeError exception is thrown.
Here's an example of what happens if hint is "number" for an array literal:
console.log([].valueOf()); // [] not a primitive, call next method ❌
console.log([].toString()); // "" primitive, return this ✔️
Now let's look at how the ToPrimitive function connects ApplyStringOrNumericBinaryOperator to OrdinaryToPrimitive.
ToPrimitive ( input [ , preferredType ] )
where preferredType is optional and can be either STRING or NUMBER. ToPrimitive can be simplified to the following:
If
inputis not an object, return it as-is.If
inputhas aSymbol.toPrimitivemethod, then invoke it with
hintas its argument and return the result.If
inputdoesn't have aSymbol.toPrimitivemethod, then return
OrdinaryToPrimitive(input, preferredType).
Here's how preferredType maps to hint in step 3.
preferredType |
hint |
|---|---|
| undefined | "number" |
| STRING | "string" |
| NUMBER | "number" |
There are only two built-in JavaScript objects that have a Symbol.toPrimitive method by default. They are Symbol and Date.
Remember, as Note 1 said, no hint is provided to ToPrimitive and Dates handle the absence of a hint as if string were given. This is because when no hint is provided, Date.prototype[Symbol.toPrimitive](hint) internally passes hint = "string" to OrdinaryToPrimitive.
This means that "toString" is the first method called, and by default Date.prototype.toString returns a human-readable string representing the date and time in the local time zone.
console.log((new Date()).toString()); // "Mon May 11 2026 12:26:17 GMT+0000 (Coordinated Universal Time)"
This returned string means there's no need to call the second method "valueOf", but it also means that the expression will be caught by the 'String Gate'.
Here is the corresponding tip:
If an additive expression contains a Date, it will evaluate to a string.
console.log(true + new Date()); // "trueMon May 11 2026 12:28:00 GMT+0000 (Coordinated Universal Time)"
console.log(1 + new Date()); // "1Mon May 11 2026 12:28:29 GMT+0000 (Coordinated Universal Time)"
Dates are caught by the 'String Gate'.
Common objects
Let's look at the other objects. Those that don't have a Symbol.toPrimitive method.
Remember that no hint is passed to ToPrimitive and consequently, none is passed to OrdinaryToPrimitive. (All standard objects except Dates handle the absence of a hint as if NUMBER were given;). This means that "valueOf" will be executed first in OrdinaryToPrimitive.
Let's look at what happens when you invoke "valueOf" on two common groups: Object and array literals ({} or []) and primitive wrapper objects (like Number() or String()).
Object and array literals return themselves when their "valueOf" method is invoked, which doesn't pass the primitive check. So, that leaves "toString" which, as you might have guessed, returns a string:
console.log([1,2].valueOf()); // [1,2] - not a primitive, call next method ❌
console.log([1,2].toString()); // "1,2" - primitive, return this ✔️
console.log({}.valueOf()); // {} - not a primitive, call next method ❌
console.log({}.toString()); // "[object Object]" - primitive, return this ✔️
Here's the corresponding tip:
If an additive expression contains an object or array literal, it will evaluate to a string.
Some examples:
console.log(1 + [2,3]); // "12,3"
console.log([1,2] + [2,3]); // "1,22,3"
console.log({a: 1} + {b: 2}); // "[object Object][object Object]"
Object and array literals are caught by the 'String Gate'.
Primitive wrapper objects, on the other hand, unwrap when the "valueOf" method is invoked. Most returned values pass the primitive check and are returned by OrdinaryToPrimitive. No need to call "toString":
console.log((new String("4")).valueOf()); // "4" - primitive, return this ✔️
console.log((new Number(5)).valueOf()); // 5 - primitive, return this ✔️
This means that with the exception of String, most primitive wrapper objects can pass the 'String Gate'.
I hope it has become clear that most of the common combinations of types don't make it past the 'String Gate'. Let's go beyond the 'String Gate' now.
Twin Gate
Here's some more of the ApplyStringOrNumericBinaryOperator function:
1. If `opText` is +, then
a. Let `lPrim` be ? `ToPrimitive(lVal)`.
b. Let `rPrim` be ? `ToPrimitive(rVal)`.
c. If `lPrim` is a String or `rPrim` is a String, then
i. Let `lStr` be ? `ToString(lPrim)`.
ii. Let `rStr` be ? `ToString(rPrim)`.
iii. Return the string-concatenation of `lStr` and `rStr`.
d. Set `lVal` to `lPrim`.
e. Set `rVal` to `rPrim`.
2. NOTE: At this point, it must be a numeric operation.
3. Let `lNum` be ? `ToNumeric(lVal)`.
4. Let `rNum` be ? `ToNumeric(rVal)`.
5. If `SameType(lNum, rNum)` is false, throw a TypeError exception.
If 1c was the 'String Gate', then step 5 is the 'Twin Gate'. First, I want to highlight the assertion at step 2, which lets us know that from this point forward, only instances of Number and BigInt are acceptable. And the 'Twin Gate' tells us that both operands must evaluate to the same type for evaluation to proceed.
So that gives us our last tip for reasoning about types.
An additive expression that contains numeric operands with different types will throw a
TypeError.
console.log(1 + 1n); // ERROR!
If you haven't realized by now, JavaScript coercion can be tricky. To predict the result of additive expressions, it helps to take a look at ToNumeric.
1. Let `primValue` be ? `ToPrimitive(value, number)`.
2. If `primValue` is a `BigInt`, return `primValue`.
3. Return ? `ToNumber(primValue)`.
So, Numbers and BigInts get returned as-is. Everything else goes to ToNumber. For our purposes, here's what ToNumber does:
| Data Type | Outcome |
|---|---|
| Number | Returns as-is |
| Symbol or BigInt | Throws a TypeError |
| undefined | Returns NaN |
| null or false | Returns 0 |
| true | Returns 1 |
| String | Returns parsed Number or NaN |
| Object | Returns ToPrimitive(argument, NUMBER) |
Building on the previous tips allows us to predict the outcome of some more combinations:
console.log(1 + undefined); // NaN
console.log(true + true); // 2
console.log(true + false); // 1
console.log(2 + null); // 2
Summary
Here are the tips once more:
If an additive expression contains a string, it will evaluate to a string.
If an additive expression contains a Date, it will evaluate to a string.
If an additive expression contains an object or array literal, it will evaluate to a string.
An additive expression that contains numeric operands with different types will throw a
TypeError.
With these tips and some practice, I hope you'll be able to predict the outcome of additive expressions more confidently. Give this one a try:
console.log(1 + {}) //"What is the result of the following expression?"
Thanks for reading.
Top comments (0)