import {NOTE} from './_util'

Type detection

JavaScript is a loosely typed language. This means that most of the time, you don't care about types. Not that there are no types, mind you, but it's just that there's no syntax for you to explicitly specify types. If you come from a statically typed language, this may sound blasphemous, but try to think about it as one less thing to worry about. While this is generally true, there are occasionally cases, where you have to know what type some input is in order to take appropriate action, and that's where this module's content comes in handy.

Before we go into type detection itself, let's talk about types in JavaScript.

There are two types of values in JavaScript: primitive values and objects. Primitive values are:

  • boolean (true, false)
  • null
  • undefined
  • string
  • symbol (since ECMAScript 6)

The primitive types can generally be obtained using the typeof function, which returns the name of the primitive value's type.

console.log('type of true:', typeof(true)) // boolean
console.log('type of 1:', typeof(1)) // number
console.log('type of null:', typeof(null)) // object
console.log('type of undefined:', typeof(undefined)) // undefined
console.log('type of string:', typeof('hello')) // string
console.log('type of Symbol("hello"):', typeof(Symbol('hello'))) // symbol

In both the browsers and on NodeJS, you will notice something strange. When you call typeof() on null you will get "object" instead of "null". This is generally a big omission in the JavaScript specification, and null is the only primitive value whose type you cannot test with typeof. Luckily, null is the only value of null type, so you can simply test for equality:

let nothing = null;
console.log('null === null:', nothing === null) // true

There is no difference between floating point numbers and integers. They are both number type.

console.log('type of 1.344:', typeof(1.344)) // number

NOTE: typeof(x) can also be written as typeof x, and there is no difference.

Functions are the only non-primitive type that you can detect using typeof() and it returns "function" as expected.

console.log('type of function:', typeof(() => {}))

When it comes to built-in non-primitive types such as Arrays, typeof() always returns object.

console.log('type of [1, 2, 3]:', typeof([1, 2, 3])) // object
console.log('type of {a: 12}:', typeof({a: 12})) // object
console.log('type of new Date():', typeof(new Date())) // object
console.log('type of /regexp/:', typeof(/regexp/)) // object

There are a few ways to check the type of these objects. One of the common methods is to use the instanceof operator.

console.log('/regexp/ is instance of RegExp:', /regexp/ instanceof RegExp)

This also works with custom constructors that you may create.

class MyCtor { }
let my = new MyCtor
console.log('my is instance of MyClass:', my instanceof MyCtor) // true

It does not work, however, if you are doing prototypal inheritance some other method.

const myBase = {
  foo: 12
}

const myFactory = () => {
  const newMy = {}
  Object.setPrototypeOf(newMy, myBase)
  return newMy
}

my = myFactory()

try {
  console.log('my is instance of myBase:', my instanceof myBase) // throws
} catch (e) {
  NOTE('Error while testing whether my is instance of myBase')
}

To check whether my inherits from myBase, you need to use the isPrototypeOf() function instead:

console.log('myBase is prototype of my:', myBase.isPrototypeOf(my)) // true

We mentioned that instanceof operator is one of the ways to test the type of built-in non-primitives. Another way is a bit hackish, but you will see that it is surprisingly useful. Every plain object has a toString() function. For example:

console.log('({n: 12}).toString():', ({n: 12}).toString()) // [object Object]

This function, when applied to objects other than the plain objects, gives us information about their type. Since this function is a property on the objects themselves, we can access it as Object.prototype.toString() (see the proto module for more information on how this works).

const toString = Object.prototype.toString
console.log('toString() on [1,2,3]:', toString.call([1, 2, 3])) // [object Array]
console.log('toString() on /regexp/', toString.call(/regexp/)) // [object RegExp]
console.log('toString() on new Date()', toString.call(new Date())) // [object Date]

Interesting thing about this approach is that it also works on primitive types.

console.log('toString() on true:', toString.call(true)) // [object Boolean]
console.log('toString() on 1:', toString.call(1)) // [object Number]
console.log('toString() on "hello":', toString.call('hello')) // [object String]
console.log('toString() on null:', toString.call(null)) // [object Null]
console.log('toString() on undefined:', toString.call(undefined)) // [object Undefined]

Not only does it work on primitive types, but it is also able to differentiate between null and other objects, so it's a double-win. Since the whole business of using Object.prototype.toString.call() is a bit too much, we'll wrap it in a function and also clean up the output a little.

const what = obj => {
  return Object.prototype.toString.call(obj)
    .replace(/\[object ([^\]]+)\]/, '$1')
}

NOTE: When using the replace() function on string objects, we can use capture groups in regular expressions, and then use the captured groups in the replacement string using $N notation, where N is the number of the group.

Now let's take this function for a spin:

console.log('what(true):', what(true)) // Boolean
console.log('what(1):', what(1)) // Number
console.log('what(null):', what(null)) // Null
console.log('what(undefined):', what(undefined)) // Undefined
console.log('what("hello"):', what('hello')) // String
console.log('what({}):', what({})) // Object
console.log('what([]):', what([])) // Array
console.log('what(new Date):', what(new Date)) // Date
console.log('what(/regexp/):', what(/regexp/)) // RegExp

Perfect! Well... not quite.

console.log('what(1 / 0):', what(1 / 0)) // Number

Well, clearly 1 divided by 0 cannot be a Number.

console.log('type of 1 / 0:', typeof(1 / 0)) // NaN (but it can also be number)

JavaScript has failed us again! :( We can fix this by handling NaN values separately in our what() function, of course, but I'll leave that as an exercise for the reader. NaN is most reliably tested for using the isNaN() function, by the way.

As mentioned before, objects that use simple prototypal inheritance using Object.create() and Object.setPrototypeOf() are not subject to this kind of type detection since they are all plain objects, but that's not a big issue in the real world where you rarely care about inheritance chain.

We've covered the how of type detection, and now it's time to talk about why. The rule of the thumb is: don't use it. In rare cases, you will still need to do it, simply because some other solution may be orders of magnitude more difficult, and your job is to get things done after all, not adhere to rules set out by random guy writing about JavaScript on the Internet.

One common reason for type checking is to hunt down unspecified arguments. Nine out ten times, you can get away with something like !param to test whether the argument has been passed. But there are cases where this won't work. Let's say we have a function that looks like this:

let logNum = num => {
  if (!num) {
    console.log('You did not pass a number')
  } else {
    console.log('You passed ' + num)
  }
}

logNum(12) // You passed 12
logNum(2) // You passed 2
logNum(0) // You did not pass a number (oops!)

Let's fix this:

logNum = num => {
  if (num === undefined) {
    console.log('You did not pass a number')
  } else {
    console.log('You passed ' + num)
  }
}

logNum(12) // You passed 12
logNum(2) // You passed 2
logNum(0) // You passed 0

Now let's say our manager comes to us and asks us to implement logNum() so that it takes an array of numbers and logs them all out.

logNum = num => {
  if (num === undefined) {
    console.log('You did not pass a number')
  } else if (Array.isArray(num)) {
    num.forEach(logNum)
  } else {
    console.log('You passed ' + num)
  }
}

logNum(12) // You passed 12
logNum([1, 2, 3])
// You passed 1
// You passed 2
// You passed 3

What? I didn't tell you about Array.isArray()? Right, testing if something is an array is so common that there is a shortcut for it. You can still use our what() function for this if you want, but it's much better to use it if you have multiple types you need to test for. In this particular case, using Array.isArray() makes more sense.

Prior to ECMAScript 6, setting default argument values was also done using type detection. For instance:

let increment = x => {
  if (x === undefined) x = 0
  return x + 1
}

NOTE: The above example is actually a very good case where you cannot avoid type detection because you can add 1 to undefined, in which case you would get a NaN as a result, and your code will not throw an exception. It makes sense mathematically in a way, but hardly useful in real life.

With ES6, it is now possible to specify the default value in the function signature:

increment = (x = 0) => x + 1

Duck typing is the opposite of type detection. Instead of checking whether the thing is a duck, we check that it quacks, or has feathers, whatever aspect are interested in. For instance, if your function takes an object, and you need to invoke foo() on the object, you may test that it is an object, then test that the foo property is a function, and then you finally call it. Or, with duck typing, you could simply call it and pray for the best. Although the latter approach sounds like a better fit for religious people, it works surprisingly well in many, many cases. Still, duck typing in JavaScript does not work as well as it does in, say, Python. The main reason for this is that trying to access a missing property on an object evaluates to undefined without throwing an exception. Here's an example:

const logStuff = thing => console.log('thing.prop ===', thing.prop)
logStuff({wrongProp: 12}) // undefined

The code appears to work, but the result is not what we intended. You may think that the solution for this is to test whether the prop is not undefined. That's one way to look at it, sure. There is also another way, which is to test whether the object has such a key using the in operator. For example:

my = {}
console.log('my has "foo" key?', 'foo' in my) // false

If we want the property to be of a specific type, we could also test for that type rather than testing that it is not undefined. This also makes our code clearer as it shows our intent.

If all of this sounds a bit scary, don't worry about it. As I keep saying, most of the time, you don't have to worry about it. 9 out of 10 times, you don't have to concern yourself with types at all.

results matching ""

    No results matching ""