A Comprehensive Guide to JavaScript Type Checking: Methods, Limitations, and Best Practices
This comprehensive guide examines JavaScript type-checking mechanisms such as typeof, instanceof, constructor, Array.isArray, and Object.prototype.toString, detailing their underlying principles, cross-realm limitations, prototype chain behaviors, and optimal implementation strategies for reliable frontend development.
As frontend developers, we commonly use typeof and instanceof to determine JavaScript object types at runtime. For primitive types (Null, Undefined, Boolean, Number, String, Symbol, BigInt), typeof generally works, except for null which historically returns "object".
console.log(undefined); // undefined
console.log(null); // object
console.log(1.0); // number
console.log(true); // boolean
console.log('hello'); // string
console.log(Symbol()); // symbol
console.log(100n); // bigintFor objects and functions, typeof returns "object" or "function", making it unsuitable for distinguishing specific object types. Arrays, plain objects, Dates, and RegExps all return "object".
const arr = [];
const obj = {};
const date = new Date();
const regexp = /a/;
console.log(typeof arr); // object
console.log(typeof obj); // object
console.log(typeof date); // object
console.log(typeof regexp); // objectThe instanceof operator checks the prototype chain. It returns true if the constructor's prototype exists anywhere in the object's prototype chain, meaning it matches parent classes as well.
const arr = [];
const obj = {};
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true
console.log(obj instanceof Array); // false
console.log(obj instanceof Object); // trueThis behavior extends to ES6 classes and prototype-based inheritance. Modifying an object's prototype chain directly alters instanceof results.
class Base {}
class Current extends Base {}
const obj = new Current();
console.log(obj instanceof Current); // true
console.log(obj instanceof Base); // true function Foo() {}
function Bar() {}
Bar.prototype = new Foo();
const obj = new Bar();
console.log(obj instanceof Bar); // true
console.log(obj instanceof Foo); // true function Other() {}
obj.__proto__ = new Other();
console.log(obj instanceof Other); // true
console.log(obj instanceof Foo); // falseLimitations of instanceof: In multi-realm environments (e.g., iframes), instanceof fails because each realm has its own global constructors. An array from the parent window will not be an instance of the iframe's Array constructor. typeof remains unaffected as it compares strings.
var arr = [1, 2, 3];
console.log(arr instanceof Array); // true
var sandbox = document.createElement('iframe');
document.body.append(sandbox);
sandbox.contentDocument.open();
sandbox.contentDocument.write(`<script>
console.log(parent.arr); // 1,2,3
console.log(parent.arr instanceof Array); // false
</script>`);
sandbox.contentDocument.close();Using constructor: To match only the exact type, constructor can be used. However, it shares the same multi-realm limitation. While constructor.name can bypass realm issues by comparing strings, it is unreliable due to anonymous classes, module aliasing, and code minification.
const arr = [];
console.log(arr.constructor === Array); // true
console.log(arr.constructor === Object); // false parent.arr.constructor.name === 'Array' class Foo {}
console.log(Foo.name); // Foo
const foo = new Foo();
console.log(foo.constructor === Foo); // true
console.log(foo.constructor.name === 'Foo'); // true const MyClass = (function() {
return class {}
}());
console.log(MyClass.name); // ''Array.isArray: This method reliably checks arrays across realms and supports derived classes. It inspires custom type-checking patterns using Symbol.for() to attach globally unique tags to instances, ensuring safe cross-realm identification.
class MyArray extends Array {}
const arr1 = [];
const arr2 = new MyArray();
console.log(Array.isArray(arr1), Array.isArray(arr2)); // true, true var arr = [1, 2, 3];
var sandbox = document.createElement('iframe');
document.body.append(sandbox);
sandbox.contentDocument.open();
sandbox.contentDocument.write(`<script>
console.log(Array.isArray(parent.arr)); // true
</script>`);
sandbox.contentDocument.close(); class Foo {
static isFoo(obj) {
return !!obj.isFooInstanceTag;
}
get isFooInstanceTag() {
return true;
}
} const instanceTag = Symbol.for('check_is_Foo_instance_tag');
class Foo {
static isFoo(obj) {
return !!obj[instanceTag];
}
get [instanceTag]() {
return true;
}
}stringTag: Historically, Object.prototype.toString.call() was used to extract internal type tags like "[object Array]". It works well for built-in objects but cannot distinguish primitives from their boxed wrappers. Since ES2015, developers can customize this output using the Symbol.toStringTag getter.
var ostring = Object.prototype.toString;
function isArray(it) {
return ostring.call(it) === '[object Array]';
} const ostring = Object.prototype.toString;
console.log(ostring.call(/a/)); // [object RegExp]
console.log(ostring.call(new Date())); // [object Date] const ostring = Object.prototype.toString;
console.log(ostring.call(1.0)); // [object Number]
console.log(ostring.call(new Number(1.0))); // [object Number] class Foo {
get [Symbol.toStringTag]() {
return 'Foo';
}
}
const foo = new Foo();
console.log(Object.prototype.toString.call(foo)); // [object Foo]ByteFE
Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.