Beyond typeof and instanceof: simplifying dynamic type checks

[2017-08-18] dev, javascript, esnext, es proposal, typedjs
(Ad, please don’t block)

This blog post describes a technique for making instanceof applicable to more values (on the right-hand side). Specifically, for primitive values.

Background: typeof vs. instanceof  

In JavaScript, you have to choose when it comes to checking the type of a value. The rough rule of thumb is:

  • typeof checks if a value is an element of a primitive type:
    if (typeof value === 'string') ···
    
  • instanceof checks if a value is an instance of a class or a constructor function:
    if (value instanceof Map) ···
    
    (Additionally, value.constructor and value.constructor.name are occasionally useful.)

This is already less than ideal, because you have to keep the difference between primitive values and objects in mind, which you can often ignore, otherwise.

Alas, a few quirks make things even more complicated:

  • typeof null is 'object', not 'null'. This is considered a bug.
  • typeof distinguishes between objects and functions (which are also objects):
    > typeof {}
    'object'
    > typeof function () {}
    'function'
    
    This quirk, combined with the previous quirk means that there is no simple way to check for object-ness via typeof.
  • Not all objects are instances of Object:
    > Object.create(null) instanceof Object
    false
    

Enabling instanceof for primitive values  

Given the class PrimitiveNumber, the following code configures for which values x the expression x instanceof PrimitiveNumber returns true. It does so by implementing a static method for PrimitiveNumber whose key is the public symbol Symbol.hasInstance.

class PrimitiveNumber {
    static [Symbol.hasInstance](x) {
        return typeof x === 'number';
    }
}
console.log(123 instanceof PrimitiveNumber); // true

Dynamic type checking via the TypeRight library  

TypeRight is a minimal library for dynamic type checking. Among other features, it uses the approach shown in the previous example to implement the following pseudo-classes. Their only purpose is to be right-hand sides of the instanceof operator:

  • PrimitiveUndefined
  • PrimitiveNull
  • PrimitiveBoolean
  • PrimitiveNumber
  • PrimitiveString
  • PrimitiveSymbol

TypeRight does not currently provide a class for checking if a value is an object, but that could easily be added.

Building on these foundations, you can use TypeRight to check if the parameters of a function have the proper types:

import * as tr from 'type-right';

function dist(x, y) {
    tr.force(x, tr.PrimitiveNumber, y, tr.PrimitiveNumber);
    return Math.hypot(x, y);
}
dist(3, 4); // 5
dist(3, undefined); // TypeError

Other approaches for simplifying type checks  

Two proposals for handling type checks are currently at stage 0. That means that they describe ideas that may or may not be explored further in the future.

Pattern matching  

The proposal “ECMAScript Pattern Matching Syntax”, by Brian Terlson and Sebastian Markbåge, introduces the key Symbol.matches to make values “matchable”:

match (val) {
    MyClass:
        console.log('val is an instance of MyClass');
}

The property could either be generated automatically or added manually, as follows (line A).

class PrimitiveNumber {
    static [Symbol.matches](x) { // (A)
        return x instanceof this;
    }
}

Builtin.is() and Builtin.typeOf()  

The proposal “Builtin.is and Builtin.typeOf” by James M. Snell introduces several mechanisms for type checking.

Builtin.is(value1, value2) checks if value1 and value2 refer to the same builtin constructor. It takes into consideration that value1 and value2 may come from the current realm or another realm:

> Builtin.is(Date, vm.runInNewContext('Date'))
true
> Builtin.is(Date, Date)
true

Builtin.typeOf() can be seen as an extension typeof that works for both primitive values and built-in classes:

Builtin.typeOf(undefined); // 'undefined'
Builtin.typeOf(null); // 'null'
Builtin.typeOf(123); // 'number'

Builtin.typeOf(new Number()); // 'Number'
Builtin.typeOf([]); // 'Array'
Builtin.typeOf(new Map()); // 'Map'

// The builtin counts, not the user-defined class
class MyArray extends Array {}
Builtin.typeOf(new MyArray()); // 'Array'

Further reading