Navigating the Complexities of the instanceof Operator in JavaScript
Written on
Chapter 1 Understanding instanceof
Throughout my journey as a software developer, particularly starting as a Java Developer, I've frequently utilized the instanceof operator in both JavaScript and TypeScript. It has been my go-to tool for determining whether a variable is an instance of a specific class or even an object itself. Surely, many of you can relate!
However, I often found myself questioning the reliability of instanceof. Are there scenarios where it might mislead me, resulting in wasted time and effort? For example, consider this line of code:
const obj = {};
console.log(obj instanceof Object);
As I often remark, the workings of this operator can be more intricate than they appear.
How does instanceof function?
The instanceof operator checks if the prototype of the right operand is present in the prototype chain of the left operand. While this concept seems straightforward, there are ways to manipulate its behavior.
For instance, using the Object.setPrototypeOf method alters the object's prototype, which can disrupt the prototype chain and affect the outcome of instanceof checks. However, in my nearly ten years of experience, I haven't encountered a need to modify prototypes explicitly.
Moreover, if you assign a Symbol.hasInstance method to the prototype of the right operand, you can further influence how instanceof operates with that operand. Consider the following example:
class EveryObjectIsMyInstance {
static [Symbol.hasInstance]() {
return true;}
}
// returns true
console.log({} instanceof EveryObjectIsMyInstance);
In this case, the class EveryObjectIsMyInstance includes a static method under the Symbol.hasInstance key. Regardless of the left operand, the right operand being EveryObjectIsMyInstance will always return true.
To illustrate a similar concept with functions rather than classes, the following snippet achieves the same result:
function EveryObjectIsMyInstance() {}
Object.defineProperty(
EveryObjectIsMyInstance,
Symbol.hasInstance, {
value: () => true,}
);
// returns true
console.log({} instanceof EveryObjectIsMyInstance);
While this manipulation is intriguing, I have yet to find a valid business case for using the Symbol.hasInstance method.
Realms and instanceof
One of the primary challenges I see with the instanceof operator arises from the existence of realms. Consider a scenario where you want to execute a function without altering existing prototypes, or where you need to manage memory and execution time carefully. Node.js Virtual Machine Context allows you to do this but reveals how instanceof can fail.
Here's a practical illustration:
const hostObject = {};
const hostFunction = () => {};
const hostString = new String('');
const hostNumber = new Number(0);
const hostBoolean = new Boolean(false);
const context = createContext({
hostObject,
hostFunction,
hostString,
hostNumber,
hostBoolean,
});
const checkHostVariableInstances = () =>
[
hostObject instanceof Object,
hostFunction instanceof Function,
hostString instanceof String,
hostNumber instanceof Number,
hostBoolean instanceof Boolean,
].join(',');
const code = (${checkHostVariableInstances.toString()})();
const containerChecks = runInContext(code, context);
// true,true,true,true,true
console.log(checkHostVariableInstances());
// false,false,false,false,false
console.log(containerChecks);
In this example, the same variables yield different results when checked within the host and the container. The reason? Each realm operates with its own set of built-in objects, leading to discrepancies in the instanceof results.
Mitigating Issues with instanceof
To address the challenges posed by the instanceof operator, I advocate for steering clear of Run-Time Type Information (RTTI). While RTTI can be useful for input validation or working with third-party libraries, many developers view it as an anti-pattern. I propose three guiding principles to navigate RTTI:
- Variables should not have disjoint-union types. If a function accepts either a string or a number, it's better to standardize on one type from the start.
- Convert union types into their more generalized form when possible. For instance, prefer a function that takes only an array of strings instead of allowing both a single string and an array.
- Use wrapper types when disjoint-union types are unavoidable. If you need to pass instances of two different classes, consider a wrapper type like this:
class A {}
class B {}
type WrapperType = {
kind: 'a',
value: A,
} | {
kind: 'b',
value: B,
}
This strategy allows you to inspect the kind property and utilize TypeScript for type narrowing.
Bonus Tip
You can achieve type narrowing without using a type-discriminating property. For example, if you receive instances of two different classes:
class A {
a() {}
}
class B {
b() {}
}
You can use the 'in' operator effectively:
function handle(instance: A | B) {
if ('a' in instance) {
instance.a();} else {
instance.b();}
}
This approach cleverly combines JavaScript's capabilities with TypeScript's type-narrowing features.
Conclusion
JavaScript and TypeScript empower developers to manipulate the languages in unique ways. Understanding these intricacies helps in resolving potential issues stemming from the misuse of language features. I hope this exploration has sparked your interest in the nuances of JavaScript!
In this video, "Correctly Using instanceof Assertion In Cypress Test," you'll learn proper application techniques for the instanceof operator in testing scenarios.
The second video titled "PATTERN MATCHING for instanceof operator in Java | Java 16 Features" dives into advanced usage of the instanceof operator and its pattern matching capabilities in Java 16.