A complete guide to check data types in JavaScript

#javascript

JavaScript is such an imperfect language that you can't even rely on it for simple things like checking data types.

In JavaScript, there are 8 data types, and there are mainly 3 ways to check which type a given value is. In this post, I am going to cover what they are, when to use them, and, in my opinion, which one is the best.

Tl;dr#

  • There are no built-in ways to check data types in JavaScript that are straightforward and all-encompassing. All of the current native ways are imperfect (or just straight up buggy), so we are stuck rolling our own.
  • Forget about typeof and instanceof, the best way to check data types is to use Object.prototype.toString

the typeof operator#

The typeof operator is probably the first that comes to mind. It works fine for number, string, undefined, boolean, symbol, function but there are some pitfalls to watch out for when using typeof:

  • it is a known bug that typeof null === 'object'. The history of typeof null covers this bug in details.
  • it doesn't differentiate plain objects from other built-in objects, except for function.
    typeof []; // 'object'
    typeof {}; // 'object'
    typeof new Date(); // 'object'
    typeof /foo/; // 'object'
    

Let's move on to the next option - the instanceof operator.

the instanceof operator#

The instanceof operator checks for the constructor of an object. In other words, it tests which class created a given value.

let Car = function () {};
let benz = new Car();
benz instanceof Car; // true

Because of this, instaceof  can correctly determine types for objects, but not for primitive types.

[] instanceof Array // ✅ true
(() => {}) instanceof Function; // ✅ true
new Map() instanceof Map; // ✅ true

1 instanceof Number; // ❌ false
'foo' instanceof String; // ❌ false

Also, since instanceof checks the constructor of an object, if you modify the prototype of an object during the runtime, the result of the instanceof check can change:

const array = [];

array instanceof Array; // ✅ true

Object.setPrototypeOf(array, null);

array instanceof Array; // ❌ false

As you can see, neither typeof or instanceof is perfect and most of the time people have to leverage and combine both approaches to do type checking.

the Object.prototype.toString method#

Turns out there is a third, arguably better way to check data types in JavaScript – Object.prototype.toString. It is a method that exists on Object.prototype, which returns a string value used for the description of an object based on its Symbol.toStringTag. If you take a look at its spec, you will realize that it is actually the all-encompassing solution we have been looking for when it comes to check data types.

Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call(1); // "[object Number]"
Object.prototype.toString.call('1'); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(new String('string')); // "[object String]"
Object.prototype.toString.call(function () {}); // "[object Function]"
Object.prototype.toString.call(null); //"[object Null]"
Object.prototype.toString.call(undefined); //"[object Undefined]"
Object.prototype.toString.call(/123/g); //"[object RegExp]"
Object.prototype.toString.call(new Date()); //"[object Date]"
Object.prototype.toString.call([]); //"[object Array]"
Object.prototype.toString.call(document); //"[object HTMLDocument]"
Object.prototype.toString.call(window); //"[object Window]

With a little bit of string processing using a regexp, we can come up with the following solution that can account for all cases:

function getType(obj) {
  const lowerCaseTheFirstLetter = (str) => str[0].toLowerCase() + str.slice(1);
  const type = typeof obj;
  if (type !== 'object') {
    return type;
  }

  return lowerCaseTheFirstLetter(
    Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1')
  );
}

getType([]); // "array"
getType('123'); // "string"
getType(null); // "null"
getType(undefined); // "undefined"
getType(); // "undefined"
getType(function () {}); // "function"
getType(/123/g); // "regExp"
getType(new Date()); // "date"
getType(new Map()); // "map"
getType(new Set()); // "set"

In fact, many runtime type validation libraries use this technique under the hood, such as this one.

Note: Object.prototype.toString can still be fooled...

Since Object.prototype.toString checks for Symbol.toStringTag, if you change Symbol.toStringTag, then you can manipulate the result:

const foo = {}
foo[Symbol.toStringTag] = 'Foo'
Object.prototype.toString.call(foo) // '[object Foo]'