Published on

JavaScript Types

12 min read

I often heard that JavaScript couldn't be trusted and was poorly designed. Usually it happened after a bug was found in production, and maybe I was the one making this joke at least once. Kyle Simpson, the author of the famous book series called You Don't Know JS, makes a case against this almost common sense and argues that most of these assumptions come from a lack of knowledge or understanding of JavaScript.

Because our monkey brains always try to save energy, we create mental models for everything. However, our interpretation of reality is faulty, and we could form wrong assumptions about how the code works.

Types

In JavaScript, we have something called primitive that is not an object and has no methods or properties. They are string, number, boolean, null, undefined, symbol, and bigint.

We also have the typeof operator that returns the type of the value. Remember, it returns the value type and not the variable type! And we always get a string as a result. Here is an example extracted from MDN:

// Numbers
typeof 37 === 'number';
typeof 3.14 === 'number';
typeof Math.LN2 === 'number';
typeof Infinity === 'number';
typeof NaN === 'number'; //* Despite being "Not-A-Number"
typeof Number('1') === 'number'; // Number tries to parse things into numbers
typeof Number('shoe') === 'number'; //* including values that cannot be type coerced to a number

// Strings
typeof '' === 'string';
typeof `template literal` === 'string';
typeof '1' === 'string'; // note that a number within a string is still typeof string
typeof (typeof 1) === 'string'; // typeof always returns a string
typeof String(1) === 'string'; // String converts anything into a string, safer than toString

// Booleans
typeof true === 'boolean';
typeof false === 'boolean';
typeof Boolean(1) === 'boolean'; // Boolean() will convert values based on if they're truthy or falsy
typeof !!(1) === 'boolean'; // two calls of the ! (logical NOT) operator are equivalent to Boolean()
// Undefined
typeof undefined === 'undefined';
typeof declaredButUndefinedVariable === 'undefined';
typeof undeclaredVariable === 'undefined';

// use Array.isArray or Object.prototype.toString.call
// to differentiate regular objects from arrays
typeof [1, 2, 4] === 'object';

typeof null === 'object'; //*
typeof { a: 1 } === 'object';
typeof new Date() === 'object';
typeof /regex/ === 'object';

// The following are confusing, dangerous, and wasteful. Avoid them.
typeof new Boolean(true) === 'object';
typeof new Number(1) === 'object';
typeof new String('abc') === 'object';

// Functions
typeof function() {} === 'function';
typeof class C {} === 'function';

The case I like the most is the

NaN === 'number'
because NaN is an invalid number, but is a number! I always felt strange using zero or -1 to represent an invalid ID, for example, where NaN would be a safer choice in cases like that.

var myAge = Number("0o46");       // 38
var myNextAge = Number("39");     // 39
var myCatsAge = Number("n/a");    // NaN
myAge - "my son's age";           // NaN

myCatsAge === myCatsAge;          // false   OOPS!!!!!!!

isNaN(myAge);                     // false
isNaN(myCatsAge);                 // true
isNaN("my son's age");            // true    OOPS!

Number.isNaN(myCatsAge);          // true
Number.isNaN("my son's age");     // false

The NaN is the only value in JavaScript, that is not equal to itself, so if you need to check a NaN value use the

Number.isNaN()
method.

Coercion

Every time a type conversion happens, an abstract operation occurs. So, if we have a non-primitive value we need to turn it into a primitive. ToPrimitive is the abstract operation responsible for it. For example, whenever we convert a non-string value to a string, ToPrimitive will handle the coercion by calling toString or toNumber.

toString examples:

       null  "null"
  undefined  "undefined"
       true  "true"
      false  "false"
    3.14159  "3.14159"
          0  "0"
         -0  "0"   // *

toString (object) examples:

                  []  ""
           [1, 2, 3]  "1,2,3"
   [null, undefined]  ","
  [[[], [], []], []]  ",,,"
              [,,,,]  ",,,"

If we have an array with some content, unless it is null or undefined, the content will be shown.

                             {}  "[object Object]"
                         {a: 2}  "[object Object]"
  { toString() { return "X"; }}  "X"

toNumber examples:

         ""  0   // The root of all evil in JavaScript. *
        "0"  0
       "-0"  -0
    " 009 "  9
  "3.14159"  3.14159
       "0."  0
       ".0"  0
        "."  NaN
     "Oxaf"  175
      false  0
       true  1
       null  0   // *
  undefined  NaN

Every numeric operation where we don't have a number will invoke the abstract operation toNumber.

For any array or object, the abstract operation will try to get a primitive first by calling valueOf() and will return itself. Then it will call toString(), and only then will call toNumber(). That's why we have this:

         [""]  0   // *
        ["0"]  0
       ["-0"]  -0
       [null]  0   // *
  [undefined]  0   // *
    [1, 2, 3]  NaN
     [[[[]]]]  0   // *

toBoolean, we could just memorize the falsy column:

      Falsy	 Truthy
         ""	 "foo"
       0,-0	 23
       null	 { a: 1 }
        NaN	 [1, 3]
      false	 true
  undefined	 function() {...}

According to MDN: "If the value is omitted or is 0, -0, null, false, NaN, undefined, or the empty string (""), the object has an initial value of false". An empty array ([ ] => " " => 0) is considered truthy in this case, because the regular abstract operations does not apply here. If the value is not in the falsy column, the result will be truthy.

Corner cases

Here we have some examples of corner cases:

Number("");            //  0           OOPS!
Number("   \t\n");     //  0           OOPS!
Number(null);          //  0           OOPS!
Number(undefined);     //  NaN
Number([]);            //  0           OOPS!
Number([1, 2, 3]);     //  NaN
Number([null]);        //  0           OOPS!
Number([undefined]);   //  0           OOPS!
Number({})             //  NaN

String(-0);            //  "0"         OOPS!
String(null);          //  "null"
String(undefined);     //  "undefined"
String([null]);        //  ""          OOPS!
String([undefined]);   //  ""          OOPS!

Boolean(new Boolean(false));  //  true        OOPS!

And here one that I bet you saw happening before:

studentsInput.value = "";
// ..
Number(studentsInput.value);            //   0

studentsInput.value = "   \t\n";
// ..
Number(studentsInput.value);            //   0

One last one worth mentioning:

Number(true);              //   1
Number(false);             //   0

3 > 2;                     //   true
2 > 1;                     //   true
3 > 2 > 1;                 //   false      OOPS!

(3 > 2) > 1;
(true) > 1;
1 > 1;                     //   false

Equality - null and undefined

Double equals (==) does type conversion when comparing two things. Triple equals (===) does the same as double equals, but without type conversion. It returns false if the types are different. So basically the difference is whether we allow coercion.

The null and undefined are coercively equal, so we have the possibility of treating both as indistinguishable by using ==.

null == undefined // true
null === undefined // false

Equality - numbers, strings and boolean

If we talk about numbers, strings and booleans the algorithm prefers to do a numeric comparison. If one of them is number and the other is string, it will call toNumber on the string, so a numeric comparison happens.

The double equals only compare primitives, so if we have a non-primitive value it will be turned into a primitive. The exception is when the non-primitives are of the same type, then it just does the same as the triple equals comparison.

var workshop1Count = 42;
var workshop2Count = [42];

// if (workshop1Count == workshop2Count) // true
// if (42 == "4") // true
// if (42 === 42) // true

Here we have one more corner case for double equals. At this point, I think you can guess where the "weird" result come from.

[] == ![];      // true

var workshop1Students = [];
var workshop2Students = [];

if (workshop1Students == !workshop2Students) {} // true

// 1- if ([] == ![]) —----—> ![] == !Boolean([]) == !(true)
// 2- if ([] == false)
// 3- if ("" == false)
// 4- if (0 === 0)

How to avoid corner cases with ==

  • When either side of the double equals can be a 0, an empty string, or maybe one of those strings with nothing but whitespace in it, avoid using them.

  • Use it just with primitives. Use this only to apply pressure on the primitives.

  • Use of (x == true) or (y == false) is absolutely not advised. Let the toBoolean execute. Use triple equals if we truly can't accept that, if it absolutely must be exactly true or exactly false.

== vs ===

When types match, double equals will execute exact the same as triple equals. If we don't know the types in a comparison, we should structure our code to have previsibility when comparing values.

In scenarios where we can't know the types, triple equal is the way to go.


References: https://github.com/getify/You-Dont-Know-JS, https://frontendmasters.com/courses/deep-javascript-v3