Lesson overview · Free interview practice

Symbols

Understand unique property keys and well-known symbols.

Topics in the full lesson
  • Introduction to Symbols
  • Creating and Using Symbols
  • Well-Known Symbols
  • Practical Applications of Symbols
  • Best Practices
  • Exercises

Practice Problems

Work through a question before revealing its explanation. These questions and answers are free; Premium adds the full lesson walkthrough, examples and implementation detail.

Completion marks record your own progress, not an automatically checked result. The task type does not determine whether it is optional.

TheoryEasy

What is a Symbol

Question

What is a Symbol in JavaScript, and what guarantees does it provide? How does it differ from other primitives?

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryEasy

Well-Known Symbols

Question

What are well-known Symbols, and what do the most-asked ones (Symbol.iterator, Symbol.toPrimitive, Symbol.hasInstance) customize?

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryEasy

Enumerating Symbol Properties

Question

Why are Symbol-keyed properties hidden from Object.keys and for...in? How do you actually enumerate them?

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

TheoryMedium

Symbols as Collision-Safe Property Keys

Question

How do Symbols prevent property-name collisions? When should you use Symbol.for() instead of Symbol()?

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

CodingHard

Customizing with Well-Known Symbols: `toPrimitive` / `toStringTag` / `hasInstance`

Implement and check your solution in the stated environment before revealing the answer.

Question

Implement a Money class using Symbol.toPrimitive, Symbol.toStringTag, and a static Symbol.hasInstance method. Each hook must have an observable purpose.

Contract

  • new Money(amount, currency) stores public amount and currency properties. Accept a finite number and a currency label of exactly three uppercase ASCII letters, such as 'USD'. Zero and negative amounts are valid. This label rule is not a lookup of real currency codes.
  • Throw TypeError for an invalid constructor argument. Do not coerce inputs or provide defaults. Error wording is up to you.
  • For the 'number' and 'default' coercion hints, return amount. For the 'string' hint, return amount.toFixed(2) + ' ' + currency. Follow JavaScript's toFixed formatting, including its behavior for very large numbers.
  • Object.prototype.toString.call(value) must return '[object Money]' for a Money instance. This tag is separate from String(value).
  • candidate instanceof Money means that candidate is a non-null object, not a function, with a finite numeric amount and a currency matching the same label rule. Read properties normally, including inherited properties. Arrays with those properties count. Other objects and primitives return false.
  • The shape check may execute getters and propagate their errors. It is for trusted values, not a safe inspection of hostile objects. A matching object does not acquire Money methods or coercion hooks.

Construction validates only the initial values. Assume valid fields remain unchanged during the coercion checks. You do not need to implement arithmetic, currency conversion, setters, or subclass-specific instance checks.

Review class instance members, static members, and the three coercion hints before attempting this core practice.

Local setup

Use Node.js 24.21.0 in a new local folder. Save starter.mjs and checks.mjs, then run:

node --version
cp starter.mjs solution.mjs
node checks.mjs

The starter deliberately fails. Implement the class in solution.mjs before expecting the checks to pass. No packages, browser, network, or real money are involved.

For TypeScript, save the alternate starter as starter.mts and the following as tsconfig.json. Use TypeScript 5.9.3 in strict mode.

{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "types": [],
    "noEmitOnError": true,
    "outDir": "build"
  },
  "files": ["solution.mts"]
}
tsc --version
cp starter.mts solution.mts
tsc --project tsconfig.json
node checks.mjs ./build/solution.mjs

The checks remain JavaScript so they can exercise runtime coercion, including price + 1. TypeScript does not infer numeric operator support from a custom coercion hook. Use explicit conversions such as Number(price) in typed application code. Consult the course's environment setup guidance if either tool is unavailable.

Starters

JavaScript starter: starter.mjs

export class Money {
  constructor(amount, currency) {
    throw new Error('Implement Money')
  }

  [Symbol.toPrimitive](hint) {
    throw new Error('Implement coercion')
  }

  get [Symbol.toStringTag]() {
    throw new Error('Implement the tag')
  }

  static [Symbol.hasInstance](candidate) {
    throw new Error('Implement the shape check')
  }
}

TypeScript starter: starter.mts

export class Money {
  amount: number
  currency: string

  constructor(amount: unknown, currency: unknown) {
    throw new Error('Implement Money')
  }

  [Symbol.toPrimitive](hint: 'number' | 'string' | 'default'): number | string {
    throw new Error('Implement coercion')
  }

  get [Symbol.toStringTag](): string {
    throw new Error('Implement the tag')
  }

  static [Symbol.hasInstance](candidate: unknown): boolean {
    throw new Error('Implement the shape check')
  }
}

Visible checks

Save these checks as checks.mjs.

import assert from 'node:assert/strict'
import test from 'node:test'

const { Money } = await import(process.argv[2] ?? './solution.mjs')

test('normal: all coercion hints and the separate object tag', () => {
  const price = new Money(19.99, 'USD')
  assert.equal(price.amount, 19.99)
  assert.equal(price.currency, 'USD')
  assert.equal(price[Symbol.toPrimitive]('number'), 19.99)
  assert.equal(price[Symbol.toPrimitive]('default'), 19.99)
  assert.equal(price[Symbol.toPrimitive]('string'), '19.99 USD')
  assert.equal(+price, 19.99)
  assert.equal(Number(price), 19.99)
  assert.equal(price + 1, 20.99)
  assert.equal('' + price, '19.99')
  assert.equal(String(price), '19.99 USD')
  assert.equal(`Total: ${price}`, 'Total: 19.99 USD')
  assert.equal(Object.prototype.toString.call(price), '[object Money]')
})

test('edge: zero, negatives, and large finite amounts follow toFixed', () => {
  for (const amount of [0, -0, -2.5, 1e21]) {
    const price = new Money(amount, 'EUR')
    assert.equal(Number(price), amount)
    assert.equal(String(price), `${amount.toFixed(2)} EUR`)
  }
})

test('error: constructor arguments are validated without conversion', () => {
  for (const amount of ['5', NaN, Infinity, -Infinity, null, undefined]) {
    assert.throws(() => new Money(amount, 'USD'), TypeError)
  }
  for (const currency of ['usd', '', 'US', 'USDD', 'ÉUR', ' USD', 'USD\n', 123, null]) {
    assert.throws(() => new Money(5, currency), TypeError)
  }
})

test('normal and edge: duck typing accepts inherited shapes, not just instances', () => {
  const duck = { amount: 5, currency: 'EUR' }
  assert.equal(new Money(5, 'EUR') instanceof Money, true)
  assert.equal(duck instanceof Money, true)
  assert.equal(Object.create(duck) instanceof Money, true)
  assert.equal(Object.assign([], duck) instanceof Money, true)
  assert.equal(Object.prototype.toString.call(duck), '[object Object]')
  assert.equal(duck[Symbol.toPrimitive], undefined)
  for (const candidate of [
    null, undefined, 5, 'USD', {}, { amount: 5 },
    { amount: NaN, currency: 'USD' },
    { amount: Infinity, currency: 'USD' },
    { amount: '5', currency: 'USD' },
    { amount: 5, currency: 'usd' },
    Object.assign(() => {}, duck),
  ]) {
    assert.equal(candidate instanceof Money, false)
  }
})

test('error: ordinary property reads can execute and propagate getters', () => {
  const error = new Error('getter failed')
  const candidate = {
    get amount() { throw error },
    currency: 'USD',
  }
  assert.throws(() => candidate instanceof Money, value => value === error)
})

Take a moment to think about this before revealing the answer

Explain your reasoning or try an implementation before comparing answers.

Explore the full Symbols material

Premium includes the complete lessons and implementation references. Free practice questions remain available without a subscription.

All course tracks & premium content
From basics to advanced masterclasses
Built for JS/TS developers like you
Real-world tips & common pitfalls
Upgrade to Premium