Introspection and Type Checking in JavaScript
Introspection enables examining objects and values at runtime. This article covers type checking, property inspection, and advanced type detection patterns.
Introduction
Introspection allows:
- Runtime type detection
- Property inspection
- Method discovery
- Object analysis
- Type validation
Understanding introspection helps you:
- Build robust type checking
- Create flexible APIs
- Implement validation systems
- Debug complex objects
- Write defensive code
typeof Operator
Basic Type Detection
// โ
Good: Basic typeof usage
console.log(typeof 42); // 'number'
console.log(typeof 'hello'); // 'string'
console.log(typeof true); // 'boolean'
console.log(typeof undefined); // 'undefined'
console.log(typeof Symbol('id')); // 'symbol'
console.log(typeof BigInt(42)); // 'bigint'
// Functions
console.log(typeof function() {}); // 'function'
console.log(typeof (() => {})); // 'function'
// Objects
console.log(typeof {}); // 'object'
console.log(typeof []); // 'object'
console.log(typeof null); // 'object' (quirk!)
typeof Limitations
// โ Bad: typeof doesn't distinguish object types
console.log(typeof []); // 'object' (not 'array')
console.log(typeof {}); // 'object'
console.log(typeof null); // 'object' (not 'null')
console.log(typeof new Date()); // 'object' (not 'date')
// โ
Good: Use other methods for specific types
console.log(Array.isArray([])); // true
console.log(Object.prototype.toString.call(null)); // '[object Null]'
console.log(new Date() instanceof Date); // true
instanceof Operator
Basic instanceof Usage
// โ
Good: Check object types
class Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true
// Built-in types
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(new Date() instanceof Date); // true
console.log(/regex/ instanceof RegExp); // true
instanceof with Inheritance
// โ
Good: Check inheritance chain
class Vehicle {
constructor(type) {
this.type = type;
}
}
class Car extends Vehicle {
constructor() {
super('car');
}
}
const car = new Car();
console.log(car instanceof Car); // true
console.log(car instanceof Vehicle); // true
console.log(car instanceof Object); // true
// Check constructor
console.log(car.constructor === Car); // true
console.log(car.constructor.name); // 'Car'
instanceof Limitations
// โ Bad: instanceof fails across realms
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArray = iframe.contentWindow.Array;
const arr = new iframeArray();
console.log(arr instanceof Array); // false (different realm)
console.log(Array.isArray(arr)); // true (works across realms)
// โ
Good: Use Array.isArray for arrays
console.log(Array.isArray([])); // true
console.log(Array.isArray(new Array())); // true
Object Introspection Methods
Object.keys, values, entries
// โ
Good: Inspect object properties
const user = {
name: 'John',
age: 30,
email: '[email protected]'
};
// Get property names
console.log(Object.keys(user));
// ['name', 'age', 'email']
// Get property values
console.log(Object.values(user));
// ['John', 30, '[email protected]']
// Get key-value pairs
console.log(Object.entries(user));
// [['name', 'John'], ['age', 30], ['email', '[email protected]']]
Object.getOwnPropertyNames
// โ
Good: Get all own properties
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, ${this.name}`;
}
}
const person = new Person('John');
// Own properties only
console.log(Object.getOwnPropertyNames(person));
// ['name']
// All properties including methods
console.log(Object.getOwnPropertyNames(Person.prototype));
// ['constructor', 'greet']
Object.getOwnPropertyDescriptors
// โ
Good: Inspect property descriptors
const obj = {
name: 'John',
age: 30
};
Object.defineProperty(obj, 'id', {
value: 123,
writable: false,
enumerable: false,
configurable: false
});
console.log(Object.getOwnPropertyDescriptors(obj));
// {
// name: { value: 'John', writable: true, enumerable: true, configurable: true },
// age: { value: 30, writable: true, enumerable: true, configurable: true },
// id: { value: 123, writable: false, enumerable: false, configurable: false }
// }
Advanced Type Checking
Comprehensive Type Checker
// โ
Good: Robust type checking function
function getType(value) {
// Handle null
if (value === null) return 'null';
// Handle undefined
if (value === undefined) return 'undefined';
// Handle primitives
const primitiveType = typeof value;
if (primitiveType !== 'object') {
return primitiveType;
}
// Handle objects
if (Array.isArray(value)) return 'array';
if (value instanceof Date) return 'date';
if (value instanceof RegExp) return 'regexp';
if (value instanceof Map) return 'map';
if (value instanceof Set) return 'set';
if (value instanceof WeakMap) return 'weakmap';
if (value instanceof WeakSet) return 'weakset';
if (value instanceof Error) return 'error';
return 'object';
}
// Usage
console.log(getType(42)); // 'number'
console.log(getType('hello')); // 'string'
console.log(getType([])); // 'array'
console.log(getType({})); // 'object'
console.log(getType(new Date())); // 'date'
console.log(getType(null)); // 'null'
Type Validation
// โ
Good: Type validation function
function validateType(value, expectedType) {
const actualType = typeof value;
if (expectedType === 'array') {
return Array.isArray(value);
}
if (expectedType === 'null') {
return value === null;
}
if (expectedType === 'object') {
return value !== null && typeof value === 'object';
}
return actualType === expectedType;
}
// Usage
console.log(validateType(42, 'number')); // true
console.log(validateType('hello', 'string')); // true
console.log(validateType([], 'array')); // true
console.log(validateType({}, 'object')); // true
console.log(validateType(null, 'null')); // true
Runtime Type Checking
// โ
Good: Runtime type checking with validation
class TypeChecker {
static isNumber(value) {
return typeof value === 'number' && !isNaN(value);
}
static isString(value) {
return typeof value === 'string';
}
static isArray(value) {
return Array.isArray(value);
}
static isObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
static isFunction(value) {
return typeof value === 'function';
}
static isBoolean(value) {
return typeof value === 'boolean';
}
static isDate(value) {
return value instanceof Date;
}
static isRegExp(value) {
return value instanceof RegExp;
}
static isPromise(value) {
return value instanceof Promise;
}
static isIterable(value) {
return value != null && typeof value[Symbol.iterator] === 'function';
}
}
// Usage
console.log(TypeChecker.isNumber(42)); // true
console.log(TypeChecker.isArray([])); // true
console.log(TypeChecker.isPromise(Promise.resolve())); // true
console.log(TypeChecker.isIterable([1, 2, 3])); // true
Property Inspection
Checking Property Existence
// โ
Good: Check if property exists
const user = {
name: 'John',
age: 30
};
// Using 'in' operator
console.log('name' in user); // true
console.log('email' in user); // false
// Using hasOwnProperty
console.log(user.hasOwnProperty('name')); // true
console.log(user.hasOwnProperty('email')); // false
// Using Object.prototype.hasOwnProperty
console.log(Object.prototype.hasOwnProperty.call(user, 'name')); // true
// Using optional chaining
console.log(user?.name); // 'John'
console.log(user?.email); // undefined
Enumerating Properties
// โ
Good: Enumerate object properties
const obj = {
a: 1,
b: 2,
c: 3
};
// for...in loop
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(`${key}: ${obj[key]}`);
}
}
// Object.keys
Object.keys(obj).forEach(key => {
console.log(`${key}: ${obj[key]}`);
});
// Object.entries
Object.entries(obj).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
Property Descriptors
// โ
Good: Inspect property descriptors
const obj = {};
Object.defineProperty(obj, 'readonly', {
value: 'immutable',
writable: false,
enumerable: true,
configurable: false
});
const descriptor = Object.getOwnPropertyDescriptor(obj, 'readonly');
console.log(descriptor);
// {
// value: 'immutable',
// writable: false,
// enumerable: true,
// configurable: false
// }
// Check if property is writable
console.log(descriptor.writable); // false
Practical Introspection Patterns
Object Cloner
// โ
Good: Deep clone using introspection
function deepClone(obj, seen = new WeakSet()) {
// Handle primitives
if (obj === null || typeof obj !== 'object') {
return obj;
}
// Handle circular references
if (seen.has(obj)) {
return obj;
}
seen.add(obj);
// Handle arrays
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item, seen));
}
// Handle dates
if (obj instanceof Date) {
return new Date(obj.getTime());
}
// Handle objects
const cloned = {};
for (const key of Object.keys(obj)) {
cloned[key] = deepClone(obj[key], seen);
}
return cloned;
}
// Usage
const original = {
name: 'John',
hobbies: ['reading', 'coding'],
date: new Date()
};
const cloned = deepClone(original);
console.log(cloned);
console.log(cloned === original); // false
console.log(cloned.hobbies === original.hobbies); // false
Object Serializer
// โ
Good: Serialize objects with introspection
class ObjectSerializer {
static serialize(obj) {
const type = this.getType(obj);
switch (type) {
case 'null':
return { type: 'null', value: null };
case 'undefined':
return { type: 'undefined' };
case 'date':
return { type: 'date', value: obj.toISOString() };
case 'array':
return {
type: 'array',
value: obj.map(item => this.serialize(item))
};
case 'object':
return {
type: 'object',
value: Object.entries(obj).reduce((acc, [key, value]) => {
acc[key] = this.serialize(value);
return acc;
}, {})
};
default:
return { type, value: obj };
}
}
static deserialize(data) {
switch (data.type) {
case 'null':
return null;
case 'undefined':
return undefined;
case 'date':
return new Date(data.value);
case 'array':
return data.value.map(item => this.deserialize(item));
case 'object':
return Object.entries(data.value).reduce((acc, [key, value]) => {
acc[key] = this.deserialize(value);
return acc;
}, {});
default:
return data.value;
}
}
static getType(value) {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (value instanceof Date) return 'date';
if (Array.isArray(value)) return 'array';
if (typeof value === 'object') return 'object';
return typeof value;
}
}
// Usage
const obj = {
name: 'John',
age: 30,
date: new Date(),
hobbies: ['reading', 'coding']
};
const serialized = ObjectSerializer.serialize(obj);
const deserialized = ObjectSerializer.deserialize(serialized);
console.log(deserialized);
API Response Validator
// โ
Good: Validate API responses
class ResponseValidator {
constructor(schema) {
this.schema = schema;
}
validate(data) {
const errors = [];
for (const [key, expectedType] of Object.entries(this.schema)) {
if (!(key in data)) {
errors.push(`Missing required field: ${key}`);
continue;
}
const actualType = this.getType(data[key]);
if (actualType !== expectedType) {
errors.push(
`Field ${key}: expected ${expectedType}, got ${actualType}`
);
}
}
return {
valid: errors.length === 0,
errors
};
}
getType(value) {
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
return typeof value;
}
}
// Usage
const userSchema = {
id: 'number',
name: 'string',
email: 'string',
tags: 'array'
};
const validator = new ResponseValidator(userSchema);
const validResponse = {
id: 1,
name: 'John',
email: '[email protected]',
tags: ['admin', 'user']
};
const invalidResponse = {
id: '1', // Wrong type
name: 'John'
// Missing email and tags
};
console.log(validator.validate(validResponse));
// { valid: true, errors: [] }
console.log(validator.validate(invalidResponse));
// { valid: false, errors: [...] }
Best Practices
-
Use typeof for primitives:
// โ Good if (typeof value === 'string') { } // โ Bad if (value instanceof String) { } -
Use Array.isArray for arrays:
// โ Good if (Array.isArray(value)) { } // โ Bad if (typeof value === 'object') { } -
Use instanceof for objects:
// โ Good if (value instanceof Date) { } // โ Bad if (typeof value === 'object') { } -
Validate user input:
// โ Good function processData(data) { if (!Array.isArray(data)) { throw new TypeError('Expected array'); } }
Common Mistakes
-
Assuming typeof always works:
// โ Bad if (typeof value === 'object') { // Could be null, array, or object } // โ Good if (value !== null && typeof value === 'object') { // Now it's definitely an object } -
Using instanceof across realms:
// โ Bad - fails across iframes if (arr instanceof Array) { } // โ Good - works everywhere if (Array.isArray(arr)) { } -
Not checking for null:
// โ Bad if (typeof value === 'object') { value.property; // Could throw if null } // โ Good if (value !== null && typeof value === 'object') { value.property; // Safe }
Summary
Introspection enables runtime type checking and object analysis. Key takeaways:
- typeof works for primitives
- instanceof checks object types
- Array.isArray is the standard for arrays
- Object methods inspect properties
- Combine multiple techniques for robust checking
- Validate user input always
- Handle edge cases (null, undefined)
- Use introspection for validation and serialization
Related Resources
- typeof Operator - MDN
- instanceof Operator - MDN
- Object Methods - MDN
- Type Checking - JavaScript.info
- Property Descriptors - MDN
Next Steps
- Learn about Metaprogramming Patterns
- Explore Advanced Object Manipulation
- Study Proxy Objects
- Practice type validation
- Build validation systems
Comments