Why is Javascript so weird. Understanding Javascript Coercion

Why is Javascript so weird. Understanding Javascript Coercion

Observe this behaviour

"0" == false;            // true 
"0" == "";                // false
false == "";            // true 
"" == [];                // true
false == [];            // true 
"" == {};                // false
"true" == true;            // false
"foo" == [ "foo" ];        // true

Javascript has been widely criticised by developers of all levels for having some particular unpredictable behaviour. Part of the reason for this unpredictability stems from how the javascript engine handles data types at runtime (dynamic typing).

This created so much burden for developers that a new "language" or "superset of javascript" as the creators like to call it, called Typescript was created to mitigate this problem. But dynamic typing in javascript isn't all so bad and can even be helpful as it can reduce verbosity of code. In fact, it is also very easy to understand once you learn the rules governing it.

In this article we will do just that. But first let's learn about data types

Data Types

These are the various forms in which data can exist in a program. In Javascript, there are seven built in types.

  1. number

  2. string

  3. boolean

  4. null

  5. undefined

  6. object (including arrays and functions)

  7. symbol (not covered in this article)

All of these are primitives except objects.

Dynamic typing means the interpreter can assign whatever data type it deems fit to a variable and even convert a variable's data type at runtime. This is unlike static typing (in TypeScript, Java, C++) where we tell the compiler what data type we want our variables to be, and this persist throughout the program i.e the data type never changes except we explicitly tell it to.

The conversion of one data type to another is called coercion.

Dynamic typing allows for coercion to occur at runtime.

Coercion

As previously stated, this is the conversion of data from one type to another e.g from a string to a number, etc. This can be done in one of two ways. Explicit and Implicit

Explicit Coercion:

As the name implies, this involves explicitly telling our code to convert from one data type to another using a native object. Examples of native objects are ; String(), Number(), Boolean(), etc

var a = 42;
var b = String( a );

var c = "3.14";
var d = Number( c );

b; // "42"
d; // 3.14

Note: we do not add new keyword, we do not call it as a constructor as that returns an object

Implicit Coercion : Here coercion is done without us asking for it. This usually occurs when we use a data type in an operation that requires another data type, e.g using strings as operands in mathematical operations.

var a = "3.14";
var b = a - 1;

b; // 2.14 a gets converted to a number

This is the part developers deeply resent and criticize, the ability of javascript to change our data type and hence our programs without our permission, however implicit coercion is not always evil and can help in code readability as we will see going forward.

We will now look at specific examples of explicit and implicit coercion

Explicit Coercion

We will now look at specific results from coercing data types starting with numbers.

Numbers

  1. Number to String

    Coercion of numbers to strings involves passing the number as an argument to the String() native object

     var a = 42;
     var b = String( a );
     b // "42"
     String( 0 ); // "0"
     String (NaN) // "NaN"
    
     String(Infinity) // "Infinity"
    
  2. Numbers to Boolean

    Coercion to booleans involves using the Boolean() native object.

    All numbers except 0 and Nan become true

     var a = 42
    
     var b = 0
    
     var c = NaN
    
     var d = Infinity
    
     Boolean(a) // true
    
     Boolean(b) // false
    
     Boolean(c) // false
    
     Boolean(d) // true
    

Strings

  1. Strings to numbers

    Coercion of strings to numbers is done with the Number () native object. If the string is made up of numeric data, it becomes a number, otherwise is becomes NaN. An empty string is coerced to 0 (weird).

     var a = "50"
     var b = "frontend okeke"
     var c = ""
    
     Number(a) // 50
     Number(b) // NaN
     Number(c) // 0
    
  2. Strings to Boolean

    Coercion of strings to Boolean is done with the Boolean() native object. All strings become true while empty strings become false.

     var a = "frontend okeke"
     var c = ""
    
     Boolean(a) // true
     Boolean(b) // false
    

Boolean

Booleans are easy and straightforward Boolean to String

var a = true 
var b = false

String(a) // "true"
String(b) // "false"

Boolean to Number

var a = true 
var b = false

Number(a) // 1
Number(b) // 0

Null and Undefined

The primitives null and undefined behave similarly when coerced

  1. To string

     var a = null 
    
     var b;
    
     String(a) // "null" 
    
     String(b) // "undefined"
    
  2. To number

     var a = null 
    
     var b;
    
     Number(a) // 0 
    
     Number(b) // NaN (weird)
    
  3. To boolean

     var a = null
    
     var b;
    
     Boolean(a) // false 
    
     Boolean(b) // false
    

Objects

Lastly, let's learn how objects are coerced to primitives. Objects contain two functions that aid in their coercion.

  1. valueOf() // (has to be defined explicitly)

  2. toString() //(exists in all objects)

These functions are called depending on if the object is to be coerced to a number of a string. You can already guess which is which.

However, if for a reason, one of these methods is absent, the other is consulted and whatever it returns is coerced further. i.e when coercing to a number, valueOf() is called, if it is absent, toString() is called (which returns a string) and that string is coerced further to a number. And vice versa for strings.

var a = {
    valueOf: function(){
        return 42;
    }
};

var b = {
    toString: function(){
        return "42";
    }
};
var c = {}

Number( a );            // 42
Number( b );            // 42
Number( c );            // NaN
String(a);            // "[object object]" (toString() exists on the prototype 
String( b );            // "42"
String( c );    // "[object object]"

Objects to String

The built in toString() method that exists on the prototype returns the string "[object Object]" for objects and "[ object Function]" for functions

However Arrays have a modified toString() method. When called by an array, toString() returns the array as a concatenated string, with items in the array separated by commas. Empty arrays return empty strings.

var a = [1,2,3]

String(a) // "1,2,3"
String ([]) // "" 
Number( [] );            // 0 (the empty string returned gets further coerced to 0)
Number( [ "abc" ] );    // NaN

Objects to Boolean

All objects are coerced to true, including empty objects and empty arrays.

var a = [];                
var b = {};    
var c = function(){};    

Boolean( a ); // true
Boolean( b ); // true
Boolean( c ); // true

Native Objects and Constructors

Before learning about implicit coercion, there is one thing to take note of. When coercing values with native objects, the new keyword is not used to call the object, i.e they are not called as constructor functions. This is because when called as constructors an object is always returned. This object wraps the coerced value and can bring about some weird behaviour


var a = new Boolean( false ); 
var b = new Number( 0 ); 
var c = new String( "" );

Boolean( a ) // true;
Boolean( b ) // true;
Boolean( c ) // true;

// a, b and c are objects now and not primitives anymore

Note: calling native objects as constructors is the same as calling passive primitive values as arguments to the Object class. In a way this can be considered coercion to the object type.

var a = new Number( 42 ); 
//is same as
var a = Object(42)

Implicit Coercion

As previously stated, this is when data is coerced to a different type without it being asked so.

Implicit Boolean

This occurs when operations that expect their operands to be Boolean but are something else. The operands are therefore coerced to booleans.

Examples are:

  1. The test expression in an if() statement.

  2. The test expression (second clause) in a for() header.

  3. The test expression in a while/do while loop.

  4. The test expression in a ternary operation.

  5. The left operand of the || and && logical operators.

This is perhaps the most common case of implicit coercion and even the biggest critics of javascript have enjoyed to benefit this sort of coercion offers.

Mathematical operations

Mathematical operations such as addition and subtraction usually involves two numeric operands. However when a non-numeric operands is passed in, you can guess what happens, it gets coerced to a number.

var a = "3.14";
var b = a - 1;

b; // 2.14 a gets converted to a number

Note: There is a little quirk to this behaviour. The mathematical operator for addition has a second function which is string concatenation. This second function in fact takes a higher precedence over numeric addition. The + operator only adds two operands when they are numbers, otherwise it concatenates them. That is to say if both operands are not numbers, they get coerced to strings and concatenated instead.

var a = "42"; 
var b = "0"; 
var c = 42; 
var d = 0; 
a + b; // "420" 
c + d; // 42
a + d; // "420"
b + c; // "420"

Equality Operator.

This is perhaps the most unpredictable of all javascript behaviours and also where receives the most criticism so get ready as we explore this forbidden land. The equality operator compares two operands and returns true if they are considered equal and false otherwise.

There are two types

  1. Loose equality ==

  2. Strict equality ===

It is a common misconception that loose equality operator only checks the values while strict equality checks both value and type. However, according to the javascript specification, the difference is: the strict equality operator does not allow coercion of any of the operands while the loose equality allows. To paraphrase, when comparing two values using the loose equality operator, the javascript engine coerces either or both of the operands till they are both primitives and of the same type. Phew.

Now we know this, let us now proceed further to learn the rules determining how values are coerced.

  1. When comparing a string and a number, the string is always coerced to a number

     var a = 42;
     var b = "42";
    
     a === b;    // false
     a == b;        // true ("42" gets coerced 42)
    
  2. When comparing a Boolean and any other value, the boolean is always coerced to a number

     var x = true;
     var y = "42";
    
     x == y; // false (true gets coerced to 1, and 1 is not equal to 42)
    
  3. When comparing an object and a string or number the object is always coerced to a string or number depending on the other operand

     var a = 42;
     var b = [ 42 ];
    
     a == b;    // true (42 gets coerced to "42" which is further coerced to 42)
    
  4. Null and undefined are equal to each other and nothing else

     var a = null; 
     var b;
    
     /*variables declared but not 
     assigned a value have undefined as their value */
    
     a == b;        // true 
     a == null;    // true 
     b == null;    // true 
     a == false;    // false 
     b == false;    // false 
     a == "";    // false 
     b == "";    // false 
     a == 0;        // false 
     b == 0;        // false
    
  5. Every other comparison is false

With these rules stated, we can revisit our initial paragraph and understand how that works now.

"0" == false;            // true 
// (false gets coerced to 0 and "0" gets coerced to 0)
"0" == "";                // false
//(no coercion occurs, they are both strings)
false == "";            // true 
//(false gets coerced to 0, an empty string gets coerced to 0)
"" == [];                // true
//(an empty array gets coerced to an empty string)
false == [];            // true 
/*(false gets coerced to 0, 
an empty array gets coerced to an empty string which further gets coerced to 0)*/
"" == {};                // false
//(an empty object gets coerced to "[object Object]"
"true" == true;                        // false
//true gets coerced to 1, "true" gets coerced to NaN)
"foo" == [ "foo" ];                    // true
//(["foo"] gets coerced to "foo"

Conclusion

Now that we have explored most of what you need to know about coercion it is now left for you the reader to decide you way forward. Either to accept javascript and it's minor quirks, constantly finding a way around it or to completely abandon the language and find solace elsewhere. If you decide like me to continue with our beloved javascript, the key is to understand high risk situations where unpredictability can arise in code and avoid them or explicitly coerce your data types, most especially to help readers of your code understand your code. Goodluck, keep hacking.