DEV Community

loading...
Cover image for Abstract Operations—the key to understand Coercion in JavaScript

Abstract Operations—the key to understand Coercion in JavaScript

aman_singh profile image Amandeep Singh ・9 min read

I was intrigued by one the question asked on my So you think you know JavaScript article.

 {} + []; // returns 0 ?? 🤔
Enter fullscreen mode Exit fullscreen mode

I admit that I didn't know the correct answer at that moment, but instead of blaming and criticising the JavaScript coercion and type system, I delved into the specs to find the definition of Addition operator. At first, the specs didn’t make much sense to me. I found it verbose. May be my brain wasn't trained on reading the specs. Yeah, let's be honest, how many of us read the specs when there's a question about JavaScript? We have our beloved StackOverflow. Right?

Well, I was desperate to know the answer. I didn't want to be in the category of those devs who consider coercion as some internal magic and dangerous, to be shunned or avoided.

So, this article is to share my understanding of coercion in JavaScript, and illustrate why coercion's bad reputation is exaggerated and somewhat undeserved—to flip your perspective so you can see its usefulness and power.

JavaScript Type System

JavaScript is a dynamically typed language where variables don't have types—values have types. JavaScript type system doesn't enforce that the variable always holds the same initial type it starts out with.

  // variable 'a' starts out with holding a string value type. 
  var a = 'some string'; 

  // you can change the type in the next line and it's completely valid
  // Now, the variable 'a' holds the value of type number
  a = 10;
Enter fullscreen mode Exit fullscreen mode

I always see this as one of the strongest points of the JavaScript type system. But some devs from strongly typed language may find this as a flaw in the language and object the usage of word 'type'. And I think that's one of the many reasons we are perpetually exploring the ways (Flow and TypeScript) to put a layer of type system on the language. In my opinion, it's like we are duck-taping JavaScript into a system which is not in the DNA of the language.

I believe we should always strive to learn the fundamentals and think alike JavaScript. Instead of flowing against it, let's flow towards it and see why the aforementioned question shouldn't be overhyped thinking that JavaScript is weird.

Let's quickly revisit what we know so far about JavaScript types and then we will deep dive into coercion in the later sections.

JavaScript has seven built-in types:

  • null
  • undefined.
  • string
  • number
  • boolean
  • object
  • symbol

Except object, all other types are called 'Primitives'. typeof operator is a nice built-in utility to check the types. Keep in mind that typeof always returns a string type.

typeof 'you are awesome!' // 'string'
typeof 42                 // 'number'
typeof true               // 'boolean'
typeof undefined          // 'undefined'
typeof {name: 'aman'}.    // 'object'
typeof Symbol()           // 'symbol'

------------------------
typeof function foo(){}.  // 'function'
typeof []                 // 'object'
Enter fullscreen mode Exit fullscreen mode

You will be wondering why invoking typeof on function and array return 'function' and 'object' respectively. The reason is that functions and array are subtypes of the object type. And because of this, you are able to add properties to the function and invoke some of the methods which an object type has—toString(), and valueOf().

function foo(a,b){}

// you can add any property on foo object. 
foo.someProperty = 'I am a property on foo function'; 

// function object has 'length' property set to number of formal params it takes when declared
foo.length; // 2

// invoke 'toString()' 
foo.toString(); // "function foo(a,b){}"

// invoke 'valueOf'
foo.valueOf(); // return this -> the function itself 
Enter fullscreen mode Exit fullscreen mode

Bonus: According to the typeof spec, if an object type implements an internal [[Call]] method, then typeof check returns "function". Object literals and array don't implement it, but the function does.

There are a few gotchas you need to be aware of with typeof operator. As you may have noticed that I have excluded typeof null from the list above. The reason is that null is a special case where typeof operator returns 'object'. It's the only primitive in JavaScript which is 'falsy' and returns 'object' from typeof check.

Note: null is the only primitive in JavaScript which is 'falsy' and returns an ’object' from typeof check.

typeof null; // 'object'; 
Enter fullscreen mode Exit fullscreen mode

So, how would you go about checking the null type explicitly? You may need a statement like:

var a = null; 
!a && typeof a == 'object'; // true

// Or you can use strict equality comparison
a === null; // true
Enter fullscreen mode Exit fullscreen mode

Let's consider one more quirk with typeof operator:

var a; 
typeof a; // 'undefined'
typeof b; // 'undefined'
Enter fullscreen mode Exit fullscreen mode

In JavaScript, var declared variables get assigned a value of undefined when they have no current value. And that's the reason typeof operator returns 'undefined'. But if you see we haven't declared the variable b anywhere, but typeof operator still manages to print 'undefined'. It's because JavaScript engine is playing safe and instead of returning some error, it returns undefined.

As I said knowing these difference is like aligning your mind with JavaScript engine. Every language has some corner cases. JavaScript is not an exception. Instead of making a joke about the language, I think it's crucial to understand them so that you can take better decisions in your program.

Now, let's move on to the next part of understanding coercion in JavaScript.

Coercion

Coercion aka 'type conversion' is a mechanism of converting one type to another. In statically (strongly) typed language this process happens at compile time whereas coercion is a run-time conversion for dynamically typed languages.

In JavaScript, we can have two types of coercion: "implicit" and "explicit". As the name implies, implicit coercion is the one which happens as a less obvious side effect of some intentional operation. On the contrary, the explicit conversion is obvious from the code that it is occurring intentionally.

var a = 10; 

var b = 'programmer' + a;           // implicit coercion
var c = `you owe me ${a} dollars`.  // implicit coercion

var d = String(a);                  // explicit coercion
var e = Number('42')                // explicit coercion 
Enter fullscreen mode Exit fullscreen mode

Have you ever wonder how coercion works internally? That's where things get interesting. But before we can explore the internal procedures, we need to understand some of the operations which are defined in ECMAScript 2020 section 7 called Abstract operation. These operations are not part of the language but are used to aid the specification of the semantics of JavaScript language. You can think of these operations as conceptual operations.

Abstract Operations

Every time a value conversion happens, it is handled by one or more abstract operation with some rules defined in the spec. Here we will look into three abstract operations: ToString, ToNumber and ToPrimitive.

Abstract operations are used to aid the specification of the semantics of JavaScript language.

ToString

Whenever we coerce a non-string value to a string value, ToString handles the conversion as in section 7.1.12 of the specification. Primitive types have natural stringification. The table looks like:

// ToString abstract operation (string conversion)
null ->            'null'
undefined ->       'undefined'
true ->            'true'
false ->           'false'
52 ->              '52'
Enter fullscreen mode Exit fullscreen mode

For regular object and array, the default toString() is invoked which is defined on the Object.prototype

var a = {language: 'JavaScript'}; 
a.toString(); // "[object Object]"

[].toString(); // ""
Enter fullscreen mode Exit fullscreen mode

You can also specify your own toString method to override the default return value:

var a = { language: 'JavaScript', toString(){return 'I love JavaScript'} }; 

a.toString(); // "I love JavaScript"
Enter fullscreen mode Exit fullscreen mode

ToNumber

Whenever a non-number value is supplied in an operation where a number was expected, such as a mathematical operation, ES2020 defines a ToNumber abstract operation in section 7.1.3. For example

// ToNumber abstract operation (number conversion)
true ->           1
false ->          0
undefined ->      NaN (not a valid number)
null ->           0 
Enter fullscreen mode Exit fullscreen mode

For object and array, values are first converted to their primitive value equivalent (via ToPrimitive operation) and the resulting value is then coerced into number according to the ToNumber abstract operation.

ToBoolean

ToBoolean is a little simpler than ToString and ToNumber operation as it doesn't do any internal conversion. It only performs a table lookup as mentioned in section 7.1.2.

Argument type Result
undefined false
null false
boolean return argument
number if argument is +0, -0 or NaN, return false; otherwise true
string if argument is empty string, return false; otherwise true
symbol true
object true

ToPrimitive

If we have non-primitive type (like function, object, array) and we need a primitive equivalent, ES2020 defines ToPrimitive in section 7.1.1.

ToPrimitve operation takes two arguments: input and hint(optional). If you are performing a numeric operation, the hint will be a 'number' type. And for string operation (like concatenation), the hint passed will be a string. Note that ToPrimitive is a recursive operation which means that if the result of invoking ToPrimitive is not a primitive, it will invoke again until we can get a primitive value or an error in some cases.

Now let's look at the algorithm behind the ToPrimitive operations.

Every non-primitive can have two methods available: toString and valueOf. If 'number' hint is sent, valueOf() method is invoked first. And if we get a primitive type from the result then we are done. But if the result is again a non-primitive, toString() gets invoked. Similarly, in the case of 'string' hint type, the order of these operations is reversed. If the invocation of these two operations doesn't return a primitive, generally it's a TypeError.

Visually, The order can be seen as follows:

// ToPrimitive Abstract Operation

// hint: "number" 
valueOf()
toString()

// hint: "string"
toString()
valueOf()
Enter fullscreen mode Exit fullscreen mode

To make it more clear here's the flow chart diagram of the algorithm we discussed above:

Alt Text

Now armed with this new knowledge of abstract operations, it’s the time to answer a few questions confidently.

Testing our knowledge

// Why the following expression produces '5' as a result? 
[] + 5; // '5'
Enter fullscreen mode Exit fullscreen mode

As per the specification in section, the addition operator ‘+’ performs string concatenation or numeric addition based on the argument type. If either of the argument is string, it will perform string concatenation. It's called operator overloading. Now let see how did we end up getting the string ”5”?

We were expecting a primitive type but end up getting an array as one of the argument. Consequently, ToPrimitive abstract operation is performed with "number" passed as a hint. Referring to the ToPrimitive diagram above, we can assert the following steps will take place to get the result.

  • [].valueOf() // returns [];
  • As, [] is not a primitive, engine will invoke [].toString() resulting in an empty string.
  • Now the expression reduces to "" + 5.
  • As we mentioned that addition operator performs string concatenation when either of argument is a string type.
  • So, 5 will be implicitly coerced to “5” via ToString abstract operation passing 'string' as a hint.
  • Finally the expression reduces to "" + "5" resulting in value "5".
[] + 5;               // ToPrimitive is invoked on []
// "" + 5; 
// "" + "5"; 
// "5"
Enter fullscreen mode Exit fullscreen mode

Now, that's a moment of inner satisfaction. Isn't it? I don't know about you but when I figured this out, I was delighted💡😀.

Before we wrap up, let's quickly demystify some of the following expression to strengthen our grip. I am going to reduce the expression from top to bottom (via abstract operations) to reach the result.

[] + [];            // ToPrimitive is invoked on both operands
// "" + "";
"" 
----------
[] + {};              // ToPrimitive is invoked on both operands
// "" + "[object Object]";
"[object Object]"

----------
'' - true; 
// There's no operator overloading for subtract operator. 
//ToNumber is invoked on both the operands (already primitive)
// 0 - 1; 
-1

-----------
1 < 2 < 3; 
// (1 < 2) < 3;      
// true < 3;              // ToNumber is invoked on true -> 1
// 1 < 3;
true; 

-----------
3 < 2 < 1; // true ooops! 
// (3 < 2) < 1; 
// false < 1;             // ToNumber is invoked on false -> 0
// 0 < 1; 
true
Enter fullscreen mode Exit fullscreen mode

Now is the right time to answer the question which basically led me to write this article.

{} + []; // 0 🤔??
Enter fullscreen mode Exit fullscreen mode

Here '{}' is not an empty object but just an empty block {}. So, JavaScript engine ignores it and left with + [] statement to execute. It’s a numeric operation and hence a ‘number’ hint will be passed to convert this empty array into a primitive value, which is an empty string. Finally, the empty string is coerced again via ToNumber operation leading to a value of 0. 😀

{} + [];                 // empty block is ignored
// + [];
// + '';
// + 0 ;
0
Enter fullscreen mode Exit fullscreen mode

Summary:

  • JavaScript is a dynamically typed language where values have type—not the variables.
  • Coercion aka “type conversion” is a procedure of converting one value type to another; it happens at compile time for JavaScript.
  • Coercion can be of two types: implicit and explicit.
  • Abstract operations are the keys to understanding coercion. They are not actual operation in the language but are used to aid the specification of the semantics of JavaScript language.
  • Whenever we receive a non-primitive value for an operation where a primitive type was expected, ToPrimitive abstract operation is invoked.
  • For any non-primitive, ToPrimitive invokes two methods: valueOf() and toString(). Depending upon the hint passed, valueOf() followed by toString() is invoked for the ‘number’ hint , and vice versa for “string”.

Conclusion:

Dynamic nature of JavaScript is one of its core features. Understanding how coercion works internally can help us write robust code. Every language has some quirks and it’s our responsibility as a developer to be mindful of these caveats. Instead of finding flaws, we need to strive for learning the semantics of the language and work towards it.

Hope you liked the article and if that’s a boolean true, a few ❤️ will make me smile 😍.

Discussion (13)

pic
Editor guide
Collapse
pencillr profile image
Richard Lenkovits

Extremely thorough article on JavaScript types. Great job!

Collapse
aman_singh profile image
Amandeep Singh Author

Glad to read that Richard. Thank you.

Collapse
alfredosalzillo profile image
Alfredo Salzillo

Now javascript has also the "bigint" primitive type.

Collapse
cdaracena profile image
Christian Aracena

Clear and concise, learned something new today, thanks man! Keep it up

Collapse
aman_singh profile image
Amandeep Singh Author

Thank you, Christian, for your kind words. I am glad that you learned something out of it. 😀

Collapse
agentmilindu profile image
Milindu Sanoj Kumarage

Great article! Learned a lot.

BTW, {1} + [] results in an 0. How is this possible?

Collapse
aman_singh profile image
Amandeep Singh Author

Hi Milindu,

The {} is a code block with a value of 1 inside. It has nothing to do with Addition operator outside of it. When parser reads this line it will evaluate 1 and move to another expression, which is + 0, and will output 0.

{
 // this is a block
let a = 10; 

console.log(a) // prints 10 
let b = 30; 
} 

// This is outside of the block
let a = 20; 
console.log(a); // prints 20
console.log(b); // throws a ReferenceError
Collapse
mujaddadi profile image
Taha Hassan Mujaddadi

I felt like I was reading Kyle Simpson's article :D.

Collapse
ankit9761 profile image
Ankit Kanyal

Extremely helpful saved my day.

Collapse
aman_singh profile image
Amandeep Singh Author

Glad to read that. 🙂

Collapse
offirmo profile image
Offirmo

Summary: use TypeScript and explicitly coerce!

Collapse
ravinaparab1 profile image
Ravina

For {} + [], {} is not an empty object but just an empty block; for [] + {}, {} is an object.

How did we reach these conclusions?

Collapse
aman_singh profile image
Amandeep Singh Author

Hi Ravina,

This behavior is how JavaScript engine (e.g V8) starts out by parsing the source code into an Abstract Syntax Tree(AST). For {}, JavaScript parser considers it as an empty block because this is the first thing in the statement. But in the case of [] + {}, the first thing is an array followed by Addition operator.

Here's a nice AST explorer to check out. Paste both the statement to verify yourself. 🙂