Episode 1 — Fundamentals / 1.18 — Operators and Type System

1.18.e — The typeof Operator and Type Checking

In one sentence: The typeof operator returns a string naming a value's type, but its results include the infamous typeof null === "object" bug and the inability to distinguish arrays from objects — so production code often combines typeof with instanceof, Array.isArray(), and other patterns for reliable type checking.

Navigation: ← 1.18.d — Assignment Operators · 1.18.f — Truthy and Falsy Values →


1. typeof syntax

typeof is a unary operator (not a function). Parentheses are optional but common:

typeof 42          // "number"
typeof(42)         // "number"  (same thing — parens just group the operand)

2. typeof return values for all types

ExpressionResultNotes
typeof undefined"undefined"
typeof true"boolean"
typeof 42"number"Includes NaN and Infinity
typeof 42n"bigint"ES2020 BigInt
typeof "hello""string"
typeof Symbol()"symbol"ES2015 Symbol
typeof null"object"BUG (see below)
typeof {}"object"
typeof []"object"Arrays are objects
typeof function(){}"function"Special case
typeof class{}"function"Classes are functions
console.log(typeof undefined);     // "undefined"
console.log(typeof true);          // "boolean"
console.log(typeof 42);            // "number"
console.log(typeof "hello");       // "string"
console.log(typeof Symbol("id"));  // "symbol"
console.log(typeof 10n);           // "bigint"
console.log(typeof null);          // "object"  (!)
console.log(typeof {});            // "object"
console.log(typeof []);            // "object"  (!)
console.log(typeof function(){}); // "function"
console.log(typeof NaN);           // "number"  (!)
console.log(typeof Infinity);      // "number"

3. The typeof null === "object" bug

This is the most famous bug in JavaScript. It has existed since the language's creation in 1995 and will never be fixed because too much existing code depends on it.

Historical explanation

In the original JavaScript engine, values were stored as a type tag + value. Objects had a type tag of 0. null was represented as the NULL pointer (0x00), which also had a type tag of 0. So typeof null checked the tag, saw 0, and returned "object".

How to properly check for null

const value = null;

// Wrong: typeof says "object"
console.log(typeof value === "object");  // true (misleading!)

// Correct: explicit null check
console.log(value === null);             // true

// Robust pattern: check for "real" objects
function isObject(val) {
  return val !== null && typeof val === "object";
}

console.log(isObject({}));      // true
console.log(isObject([]));      // true (arrays are objects)
console.log(isObject(null));    // false (correctly excluded)
console.log(isObject("hello")); // false

4. typeof for undeclared variables

Unlike most operators, typeof does not throw a ReferenceError for variables that have never been declared:

// This would throw: console.log(myVar);
// ReferenceError: myVar is not defined

// But typeof is safe:
console.log(typeof myVar);  // "undefined" (no error!)

Feature detection pattern

This is genuinely useful for checking if a global feature exists:

// Check if running in a browser
if (typeof window !== "undefined") {
  console.log("Running in a browser");
}

// Check if running in Node.js
if (typeof process !== "undefined" && process.versions?.node) {
  console.log("Running in Node.js");
}

// Check if a library is loaded
if (typeof jQuery !== "undefined") {
  console.log("jQuery is available");
}

// Check for modern API support
if (typeof IntersectionObserver !== "undefined") {
  // Use IntersectionObserver
} else {
  // Fallback
}

Note: With let/const, accessing a variable in its temporal dead zone (TDZ) still throws, even with typeof:

// typeof tdzVar;  // ReferenceError (if tdzVar is declared with let/const below)
// let tdzVar = 5;

However, truly undeclared variables (never declared anywhere) are safe with typeof.


5. typeof for functions

Functions get the special return value "function" even though they are technically objects:

console.log(typeof function() {});           // "function"
console.log(typeof (() => {}));              // "function"
console.log(typeof class MyClass {});        // "function"
console.log(typeof Math.max);               // "function"
console.log(typeof console.log);            // "function"
console.log(typeof Array.isArray);          // "function"

This is very useful for callback validation:

function executeCallback(callback) {
  if (typeof callback === "function") {
    callback();
  } else {
    console.warn("Expected a function, got:", typeof callback);
  }
}

executeCallback(() => console.log("Hello!"));   // "Hello!"
executeCallback("not a function");               // warning

6. instanceof for objects and constructors

While typeof tells you the primitive type, instanceof checks if an object is an instance of a specific constructor (checks the prototype chain).

console.log([] instanceof Array);           // true
console.log([] instanceof Object);          // true (Array extends Object)
console.log({} instanceof Object);          // true
console.log(new Date() instanceof Date);    // true
console.log(new Map() instanceof Map);      // true
console.log(/regex/ instanceof RegExp);     // true

// Primitives are NOT instances
console.log("hello" instanceof String);     // false (primitive, not String object)
console.log(42 instanceof Number);          // false
console.log(true instanceof Boolean);       // false

How instanceof works

instanceof walks the prototype chain of the left operand and checks if the right operand's .prototype appears anywhere in that chain:

class Animal {}
class Dog extends Animal {}

const rex = new Dog();
console.log(rex instanceof Dog);     // true
console.log(rex instanceof Animal);  // true  (Animal is in the chain)
console.log(rex instanceof Object);  // true  (Object is always at the top)

Limitations of instanceof

// 1. Does not work across frames/iframes (different global objects)
// An array from iframe1 is NOT instanceof Array in the parent window

// 2. Does not work with primitives
console.log("hello" instanceof String);  // false

// 3. Can be fooled by prototype manipulation
const fake = Object.create(Array.prototype);
console.log(fake instanceof Array);  // true (but it's not a real array)

7. Array.isArray() — why typeof fails for arrays

const arr = [1, 2, 3];

// typeof fails — arrays are "object"
console.log(typeof arr);                // "object"
console.log(typeof arr === "object");   // true (but so are plain objects!)

// instanceof works but has cross-frame issues
console.log(arr instanceof Array);      // true

// Array.isArray() — the BEST way
console.log(Array.isArray(arr));        // true
console.log(Array.isArray({}));         // false
console.log(Array.isArray("hello"));    // false
console.log(Array.isArray(new Array())); // true
console.log(Array.isArray(Array.prototype)); // true (it is actually an array)

Why Array.isArray() is preferred

MethodCross-frame safe?Handles edge cases?
typeof arr === "object"YesNo — objects also match
arr instanceof ArrayNoMostly
Array.isArray(arr)YesYes

Rule: Always use Array.isArray() to check for arrays.


8. Type-checking patterns in production code

Pattern 1: Comprehensive type checker

function getType(value) {
  if (value === null) return "null";
  if (Array.isArray(value)) return "array";
  return typeof value;
}

console.log(getType(42));          // "number"
console.log(getType("hello"));    // "string"
console.log(getType(null));        // "null"
console.log(getType([1, 2]));      // "array"
console.log(getType({}));          // "object"
console.log(getType(undefined));   // "undefined"
console.log(getType(() => {}));    // "function"

Pattern 2: Using Object.prototype.toString

The most robust type-checking technique uses the internal [[Class]] tag:

function toRawType(value) {
  return Object.prototype.toString.call(value).slice(8, -1);
}

console.log(toRawType(42));           // "Number"
console.log(toRawType("hello"));      // "String"
console.log(toRawType(true));         // "Boolean"
console.log(toRawType(null));         // "Null"
console.log(toRawType(undefined));    // "Undefined"
console.log(toRawType([]));           // "Array"
console.log(toRawType({}));           // "Object"
console.log(toRawType(new Date()));   // "Date"
console.log(toRawType(/regex/));      // "RegExp"
console.log(toRawType(new Map()));    // "Map"
console.log(toRawType(new Set()));    // "Set"
console.log(toRawType(() => {}));     // "Function"

Pattern 3: Validation helpers

const is = {
  string:    (v) => typeof v === "string",
  number:    (v) => typeof v === "number" && !Number.isNaN(v),
  boolean:   (v) => typeof v === "boolean",
  fn:        (v) => typeof v === "function",
  array:     (v) => Array.isArray(v),
  object:    (v) => v !== null && typeof v === "object" && !Array.isArray(v),
  nil:       (v) => v === null || v === undefined,
  defined:   (v) => v !== undefined,
};

console.log(is.number(42));      // true
console.log(is.number(NaN));     // false (NaN excluded)
console.log(is.object({}));      // true
console.log(is.object([]));      // false (array excluded)
console.log(is.object(null));    // false (null excluded)
console.log(is.nil(null));       // true
console.log(is.nil(undefined));  // true
console.log(is.nil(0));          // false

Pattern 4: Defensive function parameters

function processData(data, options = {}) {
  if (!Array.isArray(data)) {
    throw new TypeError(`Expected array, got ${typeof data}`);
  }
  if (typeof options !== "object" || options === null) {
    throw new TypeError(`Expected object for options, got ${typeof options}`);
  }
  if (options.callback && typeof options.callback !== "function") {
    throw new TypeError("callback must be a function");
  }

  // Safe to proceed
  const results = data.map(item => item * 2);
  if (typeof options.callback === "function") {
    options.callback(results);
  }
  return results;
}

9. Quick reference: choosing the right check

QuestionBest check
Is it a string?typeof x === "string"
Is it a number (not NaN)?typeof x === "number" && !Number.isNaN(x)
Is it a boolean?typeof x === "boolean"
Is it undefined?typeof x === "undefined" or x === undefined
Is it null?x === null
Is it null or undefined?x == null
Is it an array?Array.isArray(x)
Is it a plain object?x !== null && typeof x === "object" && !Array.isArray(x)
Is it a function?typeof x === "function"
Is it a Date?x instanceof Date
Is it a RegExp?x instanceof RegExp
Is it a Map/Set?x instanceof Map / x instanceof Set
What is it exactly?Object.prototype.toString.call(x)

Key takeaways

  1. typeof returns a string ("number", "string", "boolean", "undefined", "object", "function", "symbol", "bigint").
  2. typeof null === "object" is a historical bug that will never be fixed.
  3. typeof on undeclared variables returns "undefined" without throwing — useful for feature detection.
  4. typeof cannot distinguish arrays from objects — use Array.isArray().
  5. instanceof checks the prototype chain but does not work across frames or with primitives.
  6. Object.prototype.toString.call(value) is the most robust type-checking mechanism.
  7. In production, combine multiple checks into helper functions for clean, reliable type validation.

Explain-It Challenge

Explain without notes:

  1. Why does typeof null return "object"? Is this intentional?
  2. How is typeof useful for checking if a global variable exists?
  3. Why does typeof [] return "object", and what should you use instead to check for arrays?
  4. What is the difference between typeof x === "function" and x instanceof Function?
  5. Write a function that returns the accurate type name for any value, including "null" and "array".

Navigation: ← 1.18.d — Assignment Operators · 1.18.f — Truthy and Falsy Values →