JavaScript Interview Prep
Type System & Coercion

Symbol & BigInt

The Two Newest Primitives

LinkedIn Hook

Every Symbol you create is unique — even Symbol("id") === Symbol("id") is false.

Every integer above 2^53 - 1 silently loses precision — until you add the letter n and turn it into a BigInt.

Most devs never touch these two primitives because they look niche. They're not. Symbols power the entire iterator protocol, customize how your objects coerce into primitives, and keep metadata keys from colliding. BigInts are how you handle Twitter snowflake IDs, database IDs, and cryptography without losing digits.

In this lesson you'll learn how to create and share Symbols, the two "well-known" Symbols you'll actually use (Symbol.iterator and Symbol.toPrimitive), why JavaScript's Number type silently corrupts large integers, and every BigInt limitation — including why JSON.stringify(bigint) throws.

If an interviewer asks "how would you handle a 20-digit ID from an API?" — this lesson gives you the one-line answer.

Read the full lesson -> [link]

#JavaScript #InterviewPrep #Symbol #BigInt #ES2020 #Frontend #CodingInterview #WebDevelopment


Symbol & BigInt thumbnail


What You'll Learn

  • How Symbols guarantee uniqueness, and how Symbol.for() creates a shared global registry
  • How to use well-known Symbols (Symbol.iterator, Symbol.toPrimitive) to hook into built-in behavior
  • When to reach for BigInt, how to mix it with Number, and the 5 limitations you need to remember

Symbol — Guaranteed Uniqueness

Think of Symbol as a fingerprint. Every person's fingerprint is unique — even identical twins have different fingerprints. Similarly, every Symbol is unique, even if you give them the same description.

// Creating Symbols
const sym1 = Symbol("id");
const sym2 = Symbol("id");
console.log(sym1 === sym2); // false -- ALWAYS unique!

// The description is just a label, not an identifier
console.log(sym1.description); // "id"
console.log(sym1.toString());  // "Symbol(id)"

// Cannot use new keyword
// new Symbol("id"); // TypeError: Symbol is not a constructor

Symbol as Object Property Keys

// Symbols create truly private-ish properties
const id = Symbol("id");
const user = {
  name: "Rakibul",
  [id]: 12345  // Symbol as a computed property key
};

console.log(user.name);  // "Rakibul"
console.log(user[id]);   // 12345

// Symbol properties are HIDDEN from normal iteration
console.log(Object.keys(user));           // ["name"]
console.log(JSON.stringify(user));        // '{"name":"Rakibul"}'
for (let key in user) console.log(key);   // "name" -- symbol is hidden

// But they're NOT truly private -- you can find them:
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]
console.log(Reflect.ownKeys(user));              // ["name", Symbol(id)]

Symbol.for — Shared/Global Symbols

// Symbol.for creates SHARED symbols in a global registry
const s1 = Symbol.for("app.id");
const s2 = Symbol.for("app.id");
console.log(s1 === s2); // true -- same symbol from the registry!

// Regular Symbol vs Symbol.for
const regular = Symbol("id");
const shared = Symbol.for("id");
console.log(regular === shared); // false -- different registries

// Retrieve the key from a shared symbol
console.log(Symbol.keyFor(shared));  // "id"
console.log(Symbol.keyFor(regular)); // undefined -- not in global registry

Well-Known Symbols — Customize Built-in Behavior

// Symbol.iterator -- make any object iterable
const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { done: true };
      }
    };
  }
};

for (const num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}
console.log([...range]); // [1, 2, 3, 4, 5]

Symbol.toPrimitive — Custom Type Coercion

// Control how your object converts to primitives
const money = {
  amount: 42,
  currency: "USD",

  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case "number":
        return this.amount;                         // used in math: money * 2
      case "string":
        return `${this.amount} ${this.currency}`;   // used in template literals
      case "default":
        return this.amount;                         // used in == and +
    }
  }
};

console.log(+money);      // 42 (hint: "number")
console.log(`${money}`);  // "42 USD" (hint: "string")
console.log(money + 8);   // 50 (hint: "default")
console.log(money == 42); // true (hint: "default")

BigInt — When Numbers Aren't Big Enough

JavaScript's Number type safely represents integers only up to 2^53 - 1 (9,007,199,254,740,991). Beyond that, precision is lost:

// The problem: Number loses precision for large integers
console.log(9007199254740991);  // 9007199254740991 -- OK (Number.MAX_SAFE_INTEGER)
console.log(9007199254740992);  // 9007199254740992 -- still OK (by luck)
console.log(9007199254740993);  // 9007199254740992 -- WRONG! Precision lost!
console.log(9007199254740993 === 9007199254740992); // true -- catastrophic!

// BigInt solves this
const big = 9007199254740993n; // add 'n' suffix
console.log(big);              // 9007199254740993n -- exact!

// Or use the BigInt() function
const fromString = BigInt("9007199254740993");
console.log(fromString); // 9007199254740993n

BigInt Practical Example — Large ID Handling

// Real-world: Twitter/X snowflake IDs, database IDs
const tweetId = 1352678901234567890n;
const userId  = BigInt("9876543210123456789");

// API response handling
function processApiResponse(data) {
  // Many APIs return large IDs as strings to avoid precision loss
  const id = BigInt(data.id); // "1352678901234567890" -> BigInt
  console.log(id);
}

// BigInt arithmetic
const a = 100000000000000000n;
const b = 200000000000000000n;
console.log(a + b);  // 300000000000000000n
console.log(a * b);  // 20000000000000000000000000000000000n

BigInt Limitations

// 1. Cannot mix BigInt and Number in operations
// 10n + 5;  // TypeError: Cannot mix BigInt and other types

// Must explicitly convert:
10n + BigInt(5);  // 15n
Number(10n) + 5;  // 15 (but may lose precision!)

// 2. No Math methods
// Math.sqrt(16n);  // TypeError
// Math.max(1n, 2n); // TypeError

// 3. No decimals
// 10n / 3n -> 3n (truncated, not 3.333...)
console.log(10n / 3n);  // 3n -- integer division only

// 4. Cannot use with JSON.stringify directly
const obj = { id: 123n };
// JSON.stringify(obj); // TypeError: Do not know how to serialize a BigInt

// Workaround:
JSON.stringify(obj, (key, value) =>
  typeof value === "bigint" ? value.toString() : value
); // '{"id":"123"}'

// 5. Comparison works but with caveats
10n == 10;   // true (loose equality coerces)
10n === 10;  // false (strict -- different types)
10n < 20;    // true (comparison works across types)

Symbol & BigInt visual 1


Common Mistakes

  • Treating Symbol properties as truly private — they're hidden from Object.keys and JSON.stringify, but Object.getOwnPropertySymbols(obj) and Reflect.ownKeys(obj) expose them.
  • Mixing BigInt and Number in arithmetic — 10n + 5 throws TypeError. Convert one side explicitly with BigInt(5) or Number(10n) (knowing the precision risk).
  • Calling JSON.stringify(obj) when obj contains a BigInt — it throws. Pass a replacer that converts BigInts to strings, or use toString() before serialization.

Interview Questions

Q: What is a Symbol and why was it added to JavaScript?

Symbol is a primitive type that creates a guaranteed unique value. It was added in ES2015 primarily to create non-conflicting property keys on objects and to define well-known protocols (like Symbol.iterator for making objects iterable) without risking name collisions with existing code.

Q: What is the difference between Symbol() and Symbol.for()?

Symbol() creates a new unique symbol every time -- Symbol("id") !== Symbol("id"). Symbol.for("id") checks a global shared registry first. If a symbol with that key exists, it returns it; otherwise, it creates one. So Symbol.for("id") === Symbol.for("id").

Q: What is Symbol.toPrimitive and how does it work?

Symbol.toPrimitive is a well-known symbol that lets you define how an object converts to a primitive value. You implement it as a method that receives a hint parameter ("number", "string", or "default") and returns the appropriate primitive value based on the context.

Q: Why do we need BigInt?

JavaScript's Number type uses 64-bit floating point (IEEE 754), which can only safely represent integers up to 2^53 - 1 (9,007,199,254,740,991). Beyond that, precision is lost silently. BigInt allows arbitrary-precision integers, which is critical for large database IDs, cryptography, and financial calculations.

Q: Can you mix BigInt and Number in arithmetic?

No. Mixing them throws a TypeError. You must explicitly convert one to match the other: either BigInt(number) or Number(bigint) (with potential precision loss).

Q: Name 2 well-known symbols and their purpose.

Symbol.iterator defines how an object is iterated with for...of and the spread operator — implement a next() method returning { value, done }. Symbol.toPrimitive defines how an object is coerced to a primitive; it receives a hint ("number", "string", or "default") and returns the matching primitive.

Q: What is the maximum safe integer in JavaScript?

Number.MAX_SAFE_INTEGER is 2^53 - 1 = 9,007,199,254,740,991. Beyond this, integer arithmetic using Number silently loses precision. Use BigInt for larger integers.

Q: How do you create a BigInt?

Two ways: append the n suffix to an integer literal (42n), or call BigInt(value) with a number or string (BigInt("9007199254740993")). new BigInt() is not valid — it's not a constructor.

Q: Can you use Math.sqrt() with BigInt?

No. All Math methods throw TypeError on BigInts because they expect Number. If you need a BigInt square root you either implement it yourself (integer Newton's method) or convert to Number at the cost of precision.

Q: How do you serialize a BigInt to JSON?

JSON.stringify throws on BigInts by default. Pass a replacer that converts them to strings: JSON.stringify(obj, (k, v) => typeof v === "bigint" ? v.toString() : v). Parse them back with BigInt(str) on the receiving side.

Q: What makes Symbol unique compared to strings?

Two symbols are never equal, even with the same description — Symbol("x") !== Symbol("x"). Strings are interned: two strings with the same characters are always equal. That's why Symbols are used as property keys when you need a guaranteed non-colliding identifier (e.g. a library adding a hidden method to user objects).


Quick Reference — Cheat Sheet

SYMBOL & BIGINT -- QUICK MAP

Symbol:
  Symbol("x") !== Symbol("x")             -- always unique
  Symbol.for("x") === Symbol.for("x")     -- shared registry
  Symbol.keyFor(sym)                      -- reverse lookup (registry only)
  No `new Symbol()` -- not a constructor

  Hidden from: Object.keys, JSON.stringify, for...in
  Visible via: Object.getOwnPropertySymbols, Reflect.ownKeys

  Well-known symbols you'll use:
    Symbol.iterator   -> makes object iterable (for...of, spread)
    Symbol.toPrimitive -> custom ToPrimitive (hint: number/string/default)

BigInt:
  Syntax:    42n     |     BigInt("42")     |     BigInt(number)
  Max safe Number:   9,007,199,254,740,991 (2^53 - 1)

  Rules to remember:
    - cannot mix with Number (TypeError) -> convert explicitly
    - no Math.* methods
    - integer division only (10n / 3n -> 3n)
    - JSON.stringify throws -> use replacer that returns toString()
    - 10n == 10 is true, but 10n === 10 is false
    - comparison across types works: 10n < 20 is true

Previous: NaN, null, undefined -> The Three Empty Parking Spaces Next: Template Literals -> Strings That Actually Read Like Sentences


This is Lesson 8.6 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.

On this page