back to blogs

Parody of JavaScript types

If you’ve spent any decent amount of time working with JavaScript, you’ve likely noticed that it’s not a perfect language.

However, the most frustrating part of JavaScript for me is the inability to correctly identify the type of a value. For a language that is dynamically typed, any user of the language surely expects a robust utility or helper to assist with this.

If you’re thinking we have helpers in the form of the typeof and instanceOf operators, then you probably haven’t used JavaScript enough to understand their pain points and shortcomings, because they are not the ideal solution.

Well, if I’m being honest, I don’t really think that as of today we have a perfect solution to achieve this. However, I can surely suggest an approach that I’ve learned and find it to be the closest and most encompassing one.

The answer? It’s the toString() method available on the object prototype.

But before reaching that conclusion, let’s explore the issues with the common approaches of identifying types: typeof and instanceOf.

typeof operator

It’s the most common and widely used method. With the help of typeof operator, one can get the type of a value with the following syntax typeof x, where, x is the variable. It returns a string value denoting the type for the variable.

./src/utils/getType.js
console.log(typeof 1); // 'number'
console.log(typeof '1'); // 'string'
console.log(typeof true); // 'boolean'
console.log(typeof (() => {})); // 'function'
console.log(typeof undefined); // 'undefined'
console.log(typeof {}); // 'object'
console.log(typeof Symbol('hi')); // 'symbol'
console.log(typeof 10n); // 'bigint'

There are 8 possible values for types using this method, each of which is highlighted above.

But, there is an issue, one of which is a known legacy bug: typeof null returning an object in JavaScript.

You can read more about it on this stackoverflow thread.

But ignoring that since it’s been widely accepted, values like Map, Set , Date also return a object when used with the typeof operator.

./src/utils/getType.js
console.log(typeof 1); // 'number'
console.log(typeof '1'); // 'string'
console.log(typeof true); // 'boolean'
console.log(typeof (() => {})); // 'function'
console.log(typeof undefined); // 'undefined'
console.log(typeof {}); // 'object'
console.log(typeof Symbol('hi')); // 'symbol'
console.log(typeof 10n); // 'bigint'
console.log(typeof null); // 'object'
console.log(typeof []); // 'object'
console.log(typeof new Date()); // 'object'
console.log(typeof new Set()); // 'object'
console.log(typeof new Map()); // 'object'
console.log(typeof new RegExp()); // 'object'

Shouldn’t they have returned date, map, set following the convention, and considering function was also a possible type? Well the answer isn’t so simple.

Now, here’e the thing, outside of the classic null case, other values like Map, Set etc, are actually built-in “objects” into JavaScript. So the typeof operator returning object for them actually makes sense. But typeof (()=>{}) does not follow the same rules?

Yes 😅, since they are treated special in JavaScript and are also referred as callable action objects, i.e, we can not only call them but also treat them as objects.

Considering these above shortcomings and irregular output of the typeof operator, we can also reach out to the new instanceOf operator for rescue which we’ll discuss below.

instanceOf operator

The instanceOf operator tests to see if the prototype property of a constructor appears anywhere in the prototype chain of an object.

If you have already not figured out from the definition, it comes with two straight shortcomings:

  1. instanceOf does not work on any of the primitives types.
./src/instanceOfUsage.js
console.log(3 instanceof Number); // false
console.log('hello' instanceof String); // false
console.log(true instanceof Boolean); // false
console.log(10n instanceof BigInt); // false
// We need to use boxing technique to get the correct result
console.log(Number(3) instanceof Number); // true
console.log(String('hello') instanceof String); // true
// and so on ...

Moreover, you cannot use instanceOf with values like null or undefined as there are no constructor function available for them. 😅

  1. instanceOf does not give us the actual type but rather, confirms if something is extended from any base type (i.e, part of the prototype chain), so one still needs to run checks to determine the actual type for the value.

Apart from these shortcomings, the instanceOf operator works well to further understand if a given value belongs to any of the in-built object types (beyond just the “object”) case of the typeof operator.

./src/instanceOfUsage.js
console.log((() => {}) instanceof Function); // true
console.log(null instanceof Object); // false
console.log(new Date() instanceof Date); // true
console.log(Promise.resolve(1) instanceof Promise); // true
console.log({} instanceof Object); // true
// since both Promise and function still have Object.prototype on their prototype chain
console.log(Promise.resolve(1) instanceof Object); // true
console.log((() => {}) instanceof Object); // true


A more interesting problem here, is as the instanceOf operator checks up the prototype chain, any changes made on this chain during runtime can yield in-correct result. Although this might be less likely, but still a good case to consider.

./src/instanceOfUsage.js
const array = [];
console.log(array instanceof Array); // true
Object.setPrototypeOf(array, null);
console.log(array instanceof Array); //false 😅

Considering, the above cases now let’s visit my most preferred solution using Object.prototype.toString()

Object.prototype.toString()

The toString() method of Object instances returns a string representing this object. Funnily with a little effort it can actually be the most encompassing solution to correctly identify the type for the value.

./src/toString.js
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(/test/g); //"[object RegExp]"
Object.prototype.toString.call(new Date()); //"[object Date]"
Object.prototype.toString.call([]); //"[object Array]"
Object.prototype.toString.call(window); //"[object Window]

If we create a small utility to transform the returned value, we can get the correct type for both primitives and non-primitive values.

./src/getType.js
function getType(input) {
const type = Object.prototype.toString.call(input).replace(/^\[object (\S+)\]$/, '$1');
// lowercase the first character
return `${type[0].toLowerCase()}${type.slice(1)}`;
}
// Usage
console.log(getType(1)); // 'number'
console.log(getType('1')); // 'string'
console.log(getType(true)); // 'boolean'
console.log(getType(() => {})); // 'function'
console.log(getType(undefined)); // 'undefined'
console.log(getType({})); // 'object'
console.log(getType(Symbol('hi'))); // 'symbol'
console.log(getType(10n)); // 'bigint'
console.log(getType(null)); // 'null'
console.log(getType([])); // 'array'
console.log(getType(new Date())); // 'date'
console.log(getType(new Set())); // 'set'
console.log(getType(new Map())); // 'map'
console.log(getType(new RegExp())); // 'regExp'
console.log(getType(window)); // window
console.log(getType(document)); // document

From the above usage we can see how it is an all-encompassing solution. However, as mentioned at the start of the blog, there exist no perfect solution to get the type for a value in JavaScript today.

Since, the toString() returns a string representing the object, we can also override this value at runtime and fool the result. This however is very unlikely to occur.

// we force everything to be a number
Object.prototype.toString = function (obj) {
return 'number'; // Oops! 😅
};
// or overide just for numbers
Number.prototype.toString = function (obj) {
return 'string';
};
// and so on..

Conclusion

In conclusion, while there’s no perfect solution for accurately determining the type of a value in JavaScript, exploring methods like the typeof and instanceOf operators sheds light on their strengths and limitations.

The Object.prototype.toString() method, although not flawless, emerges as a comprehensive approach to identifying types in JavaScript, covering both primitive and non-primitive values. Each method discussed has its pros and cons, and understanding their nuances can guide developers in choosing the most suitable approach for their specific needs.

So the next time when you are sitting for a JavaScript interview and run into a situation where you want to identify the type of value you are working with, you have possible solutions to consider now.

Keep learning, stay curious, and enjoy the journey!