instanceof
in depth
instanceof
lies
Eric Elliott repeats very often that instanceof
lies. What would be expected is that running object instanceof MyObject
will return true
if object
’s type is of MyObject
. However, this doesn’t work like that in JavaScript. What is checked instead is if anywhere on the prototypes chain of object
there is MyClass
.
class MyArray extends Array {}
const myArray = new MyArray();
// now
myArray instanceof MyArray; // true
myArray instanceof Array; // true
myArray instanceof Object; // true
Seems to be fine. The problem are false positives.
The case of Array
JavaScript doesn’t have actual arrays, like other languages. Array is just a kind of object, coming with the set of methods in its own prototype. It has its literal in syntax ([]
) but that’s about it. One of the common complaints is that typeof [] == 'object'
, not 'array'
.
For a moment I was using instanceof Array
to check if something is an array but this can be tricky, as well. Consider the following,
const array = []; // []
const notArray = Object.create(Array.prototype); // Array {}
array instanceof Array; // true
notArray instanceof Array; // also true
Does it behave like an array? Let’s see.
array.push(42); // [42]
notArray.push(43); // [43] <-- though, more accurate would be: Array { 0: 43, length: 1 }
Are the objects compatible?
array.concat([43]); // [42, 43]
array.concat(notArray); // [42, Array(1)], or: [42, Array { 0: 43, length: 1 }]
// but
[...array, ...notArray]; // [42, 43], more here: [1]
Another difference are the keys. Object created with Object.create
will have length
enumerable. This might be dangerous if someone tried to iterate it with for..in
loop. notArray
object would behave differently[2].
Object.keys(array); // ["0"]
Object.keys(notArray); // ["0", "length"]
Because there were challenges with recognising array, static method Array.isArray
has been added. Let’s see how our pseudo-array is doing here.
Array.isArray(array); // true
Array.isArray(notArray); // false
After all, the only case that instanceof
is really useful is if we want to use a method that is present on the desired prototype. Nothing about being actual instance. But then again, JavaScript tends to have its own way of dealing with instances.
Clean object
Due to well-known error in the specs that were submitted in 1997, typeof null == 'object'
(it was meant to be 'null'
). For that reason I was considering for a while using object instanceof Object
instead of object !== null && typeof object == 'object'
[3]. I wrote then:
Is there any catch here? Can this be used safely?
Now I can reply to myself that yes, there is a catch. It is possible to create an object that will be object and at the same time it will fail to give true to object instanceof Object
.
const cleanObject = Object.create(null); // {} with absolutely no properties
typeof cleanObject; // 'object'
cleanObject instanceof Object; // false
I still haven’t found any use for this, as usually there is no harm in and downside of having Object.prototype
on the chain. In this case, instanceof
would work as a measure to check if we can safely use default properties of Object.prototype
which confirms conclusion from the previous section.
Spawnlings of Object.create
If object instanceof ArbitraryClass
goes up the chain of prototypes to check if our left-hand side (object
) has right-hand side (ArbitraryClass
) on its prototype chain, then is it possible to use it with results of Object.create
?
const prototype = {
print() {
console.log('I am printing');
}
};
const object = Object.create(prototype); // {}
object.print(); // "I am printing"
object instanceof prototype; // Uncaught TypeError: Right-hand side of 'instanceof' is not an object
// however,
object instanceof Object; // true
No, it’s not possible.
Method behind the method
And yet, there is no magic or incoherency of the behaviour. instanceof
is simply calling a method defined as a class static method hidden behind Symbol.hasInstance
.
class MyArray extends Array {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}
[] instanceof MyArray; // true
In other codelines, what we’re calling is this:
MyArray[Symbol.hasInstance]([]); // true
That’s why the spawnling of Object.create
identified itself as an instance of Object
but not our prototype. Let’s fix it.
const proto = {
print() {
console.log('I am printing');
},
[Symbol.hasInstance](instance) {
return Object.getPrototypeOf(instance) == proto;
},
};
const object = Object.create(proto);
object.print(); // "I am printing"
object instanceof proto; // true
object instanceof Object; // true
Run-time interfaces
This allowed me to realise that in fact instanceof
is not bound to so-called classes in JavaScript.
const LinkedListItem = {
[Symbol.hasInstance](item) {
return item.hasOwnProperty('value')
&& item.hasOwnProperty('next')
&& (
item.next === null
|| item.next instanceof LinkedListItem
);
},
};
const item1 = {
value: 42,
next: null,
};
const item2 = {
value: 63,
next: null,
};
item1.next = item2;
const somethingElse = {
name: 'Geoffrey',
};
item1 instanceof LinkedListItem; // true
item2 instanceof LinkedListItem; // true
somethingElse instanceof LinkedListItem; // false
Upon seeing this, my team mate, Riccardo, said—as more aware user of TypeScript—those are run-time interfaces. JavaScript itself doesn’t have a concept of interface but it’s actually a different name for ducktyping.
The function above could be written like this in TypeScript:
interface LinkedListItem {
value: any;
next: null | LinkedListItem;
};
Interfaces are a fantastic tool to describe a shape of object, that is what fields and what types of values should they have to be deemed valid. This is my single favourite feature from TypeScript, that I otherwise don’t hold high, and the worst thing is they are gone after transpilation. With Symbol.hasInstance
we could probably incorporate instanceof
syntax, if it’s not so useful as its promise, anyway.
But maybe it’s better to forget about it, along with other operators (like in
) and explicitly use functions. I still haven’t decided.
The pieces of the code from the article put together can be found here.
Footnotes
[1] The reason why [...array, ...notArray]
worked, while concat
didn’t, is that array spreading is internally using Symbol.iterator
method which happens to be on the Array’s prototype. But this is a story for another time.
[3] Shorter version: object && typeof object == 'object'
because null
will validate to falsy value and typeof
will do the rest.