C.W.K.
Stream
Lesson 04 of 05 · published

`symbol` and `bigint`: The Under-Used Primitives

~9 min · primitives, symbol, bigint, javascript-evolution

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"Two primitives most TypeScript programmers will never type by hand — but you need to recognize both when they appear."

symbol — guaranteed unique identifiers

A symbol is a primitive value that's guaranteed to be unique. Calling Symbol() twice with the same description produces two different symbols. They're most commonly used as object property keys when you need to attach data to an object without colliding with anyone else's keys.

You'll meet symbols most often through the built-in well-known symbols — Symbol.iterator, Symbol.asyncIterator, Symbol.toPrimitive, etc. These are how JavaScript's iteration protocols, primitive coercion, and other meta-behaviors hook into your objects. If you've ever written for (const x of obj), that loop calls obj[Symbol.iterator]() under the hood.

You can declare your own symbols (const TAG = Symbol('Tag')), use them as private-ish property keys, and even type them with the unique symbol type. But day-to-day TypeScript barely touches them directly — they show up most often in the type annotations of library APIs you consume.

bigint — arbitrary-precision integers

JavaScript's number is a 64-bit double, which means integer math past ±2^53 loses precision. bigint is the fix: an arbitrary-precision integer type that handles arithmetic of any size, with the trade-off that you can't mix it with number without an explicit conversion.

You write a bigint literal with an n suffix: const big = 9007199254740993n. Or call BigInt(value). The arithmetic operators work normally between two bigints (big * 2n), but mixing types throws: big + 1 errors at runtime with a TypeError. TypeScript will catch this at compile time if you've annotated correctly.

Use bigint when you need: timestamps in nanoseconds, IDs that exceed Number.MAX_SAFE_INTEGER (database row IDs from systems that use 64-bit identifiers), cryptography, financial calculations that must avoid floating-point error.

Both primitives exist for a reason, but neither is a daily-driver type. Most TypeScript code in 2026 uses string, number, and boolean for the everyday values. symbol and bigint are specialized tools — recognize them, use them when their problem appears, otherwise let them sit in your toolbox.

Wrapper objects exist (and you should still avoid them)

Same lowercase-vs-capital rule as the big three: : symbol is the primitive, : Symbol is the wrapper-object constructor type. : bigint primitive, : BigInt wrapper. Always lowercase.

Pippa's confession

cwkPippa's frontend uses neither. Symbols show up in the React internals I touch indirectly, and bigint would show up if I were dealing with database row IDs of a size that overflowed JavaScript number — which the cwkPippa SQLite layer doesn't reach. They're real and useful primitives; they're just narrow tools. Knowing they exist matters more than using them constantly.

Code

symbol — unique keys, iterator protocol hooks·typescript
// symbol — guaranteed unique.

const a = Symbol('id');
const b = Symbol('id');
console.log(a === b);             // false — even with the same description

// Used as a property key on an object.
const MY_KEY = Symbol('myKey');
const obj = {
  [MY_KEY]: 'private-ish value',
  publicProp: 'visible',
};

console.log(obj[MY_KEY]);         // 'private-ish value'
console.log(Object.keys(obj));    // ['publicProp'] — symbol keys are skipped

// well-known symbols make objects iterable.
class Range {
  constructor(public start: number, public end: number) {}
  *[Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i++) yield i;
  }
}

for (const n of new Range(1, 3)) console.log(n);  // 1, 2, 3
bigint — when number's precision runs out·typescript
// bigint — arbitrary-precision integers.

const small: bigint = 9007199254740992n;        // n suffix marks bigint
const big: bigint = small * 2n;                  // 18014398509481984n — exact

// number can't represent this exactly:
const asNumber: number = 9007199254740993;       // gets rounded to 9007199254740992

// Mixing fails — both at runtime and (if annotated) at compile time.
// const broken = big + 1;                       // ❌ Type 'bigint' not assignable to '1n' (BigInt + number is a runtime TypeError too)
const fixed = big + 1n;                          // ✅ both bigint
const converted = Number(big);                   // ✅ explicit cast, may lose precision

// When you'd reach for it:
//  - database IDs > 2^53
//  - nanosecond timestamps (process.hrtime.bigint() in Node)
//  - currency in smallest units, when float math is unacceptable

External links

Exercise

Write a class with an iterator: it should accept an array in the constructor and yield each element in reverse. Use a generator method with [Symbol.iterator]. Then write a for...of loop over an instance and verify it works. Bonus: try [...instance] — what do you get?
Hint
Generators (the function* / yield form) are the easiest way to write iterators. The class method is *[Symbol.iterator](). The spread operator and the for-of loop both consume the symbol-keyed iterator the same way.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.