Skip to main content
โšก Calmops

Introspection and Type Checking in JavaScript

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

  1. Use typeof for primitives:

    // โœ… Good
    if (typeof value === 'string') { }
    
    // โŒ Bad
    if (value instanceof String) { }
    
  2. Use Array.isArray for arrays:

    // โœ… Good
    if (Array.isArray(value)) { }
    
    // โŒ Bad
    if (typeof value === 'object') { }
    
  3. Use instanceof for objects:

    // โœ… Good
    if (value instanceof Date) { }
    
    // โŒ Bad
    if (typeof value === 'object') { }
    
  4. Validate user input:

    // โœ… Good
    function processData(data) {
      if (!Array.isArray(data)) {
        throw new TypeError('Expected array');
      }
    }
    

Common Mistakes

  1. 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
    }
    
  2. Using instanceof across realms:

    // โŒ Bad - fails across iframes
    if (arr instanceof Array) { }
    
    // โœ… Good - works everywhere
    if (Array.isArray(arr)) { }
    
  3. 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

Next Steps

Comments