How Lodash isSymbol Handles Primitive and Object Symbols

The Lodash _.isSymbol method checks whether a given value is classified as a Symbol primitive or a Symbol wrapper object. While native JavaScript operators like typeof struggle to categorize object-wrapped symbols consistently alongside primitive symbols, Lodash abstracts this difference away. This article explains how _.isSymbol functions under the hood, how it distinguishes and verifies both primitive and object symbols, and why this distinction matters in JavaScript development.

The Difference Between Primitive and Object Symbols

JavaScript introduces symbols primarily as primitive values:

const primitiveSymbol = Symbol('description');
typeof primitiveSymbol; // 'symbol'

However, it is also possible to wrap a symbol into an object using the Object() wrapper function:

const objectSymbol = Object(Symbol('description'));
typeof objectSymbol; // 'object'

Because typeof objectSymbol yields 'object', standard type checks using typeof value === 'symbol' fail when dealing with boxed symbol instances.

How Lodash Implements _.isSymbol

Lodash bridges this gap by combining a primitive type check with an internal tag inspection. Under the hood, _.isSymbol uses a two-pronged evaluation:

  1. Primitive Check: It first verifies if typeof value === 'symbol'. If this evaluates to true, the value is confirmed to be a primitive symbol immediately.
  2. Object Wrapper Check: If the primitive check fails, it evaluates whether the value is "object-like" (non-null and typeof value === 'object'). For object-like values, it checks the internal [[Class]] tag using Object.prototype.toString.call(value). If this returns '[object Symbol]', it confirms the value is an instance of a boxed Symbol.

The simplified implementation looks like this:

function isSymbol(value) {
  const type = typeof value;
  return type === 'symbol' || (
    type === 'object' && 
    value !== null && 
    Object.prototype.toString.call(value) === '[object Symbol]'
  );
}

Behavior and Usage Examples

Because of this dual-layer verification, _.isSymbol provides consistent results regardless of how the symbol was instantiated:

// Primitive Symbol
const primitive = Symbol('test');
_.isSymbol(primitive); 
// => true

// Object-wrapped Symbol
const boxed = Object(Symbol('test'));
_.isSymbol(boxed); 
// => true

// Non-symbols
_.isSymbol('test'); 
// => false

_.isSymbol({}); 
// => false

Cross-Realm Reliability

Using Object.prototype.toString.call(value) also ensures that _.isSymbol works reliably across different execution contexts, such as multiple iframes or Node.js vm contexts. In cross-realm environments, instanceof Symbol fails because the prototype originates from a different global execution context. By relying on the internal string tag rather than the prototype chain, _.isSymbol guarantees accurate identification for both primitive and boxed symbols regardless of their origin realm.