
In JavaScript, objects can participate in a wide variety of built-in operations by implementing specific methods or well-known symbols. Beyond the familiar
obj.valueOf() // for numeric coercion
obj.toString() // for string coercion
you can hook into almost every language feature. In this post we will explore the most useful methods and symbols, show multiple examples of each, and explain when to use them.
1. Custom Primitive Conversion with Symbol.toPrimitive
When the engine needs to coerce your object to a primitive value, say in arithmetic, string interpolation, or loose equality, it will look first for a [Symbol.toPrimitive]
method. If you define it, neither valueOf
nor toString
will be called.
class Color {
constructor(r, g, b) {
this.r = r;
this.g = g;
this.b = b;
}
[Symbol.toPrimitive](hint) {
if (hint === 'number') {
// return a grayscale equivalent
return 0.3 * this.r + 0.59 * this.g + 0.11 * this.b;
}
else if (hint === 'string') {
// return CSS rgb notation
return `rgb(${this.r},${this.g},${this.b})`;
}
// default hint; use string form
return this.toString();
}
toString() {
return `Color(${this.r},${this.g},${this.b})`;
}
}
const c = new Color(10, 20, 30);
console.log(+c); // numeric hint: 0.3*10+0.59*20+0.11*30 → 18.2
console.log(`${c}`); // string hint: "rgb(10,20,30)"
console.log(c + '!'); // default hint: "Color(10,20,30)!"
Other examples
- Use
numeric
hint to control sorting or comparison in arrays - Use
string
hint to integrate with template engines
2. Locale-Aware Strings with toLocaleString()
Many built-ins, such as Array.prototype.join
or Date.prototype.toLocaleString
, defer to your object’s toLocaleString
method. Implement it to format dates, numbers, or currencies according to locale settings.
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
toLocaleString(locale, options = {}) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: this.currency,
...options
}).format(this.amount);
}
}
const m = new Money(1234.56, 'USD');
console.log(m.toLocaleString('en-US')); // "$1,234.56"
console.log(m.toLocaleString('de-DE')); // "1.234,56 $"
console.log([m, m].join(' and ')); // uses toLocaleString, then join → "$1,234.56 and $1,234.56"
Tips
- Accept options to override locale defaults
- Use in custom collections that need human-readable output
3. Controlling JSON Serialization with toJSON()
When JSON.stringify(obj)
runs, it checks for a toJSON()
method and uses its return value instead of the original object. You can prune or reshape data for network transfer.
class User {
constructor(id, name, passwordHash) {
this.id = id;
this.name = name;
this.passwordHash = passwordHash;
}
toJSON() {
// exclude sensitive fields
return { id: this.id, name: this.name };
}
}
const user = new User(1, 'alice', 'xyz123hash');
console.log(JSON.stringify(user)); // → '{"id":1,"name":"alice"}'
Variations
- Return an array rather than an object
- Embed type metadata for deserialization
4. Custom Object Tag with Symbol.toStringTag
By default Object.prototype.toString.call(obj)
yields [object Object]
. You can override the tag to improve debugging or logging.
class Matrix {
get [Symbol.toStringTag]() {
return 'Matrix';
}
}
const mat = new Matrix();
console.log(Object.prototype.toString.call(mat)); // → "[object Matrix]"
Use cases
- Distinguish custom collections or domain models in debug logs
- Integrate with libraries that inspect object tags
5. Iteration Protocols: Symbol.iterator
and Symbol.asyncIterator
To make your object iterable in a for…of
loop or spread syntax, define [Symbol.iterator]
. For asynchronous streams, define [Symbol.asyncIterator]
.
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[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
// Async example
class AsyncRange {
constructor(limit) {
this.limit = limit;
}
async *[Symbol.asyncIterator]() {
for (let i=1; i <=this.limit; i++) {
// simulate delay
await new Promise(r=> setTimeout(r, 100));
yield i;
}
}
}
(async () => {
for await (const n of new AsyncRange(3)) {
console.log(n);
}
})();
6. Other Well-Known Symbols and Hooks
Symbol or Method | Purpose |
Symbol.hasInstance |
Customize obj instanceof Constructor |
Symbol.isConcatSpreadable |
Control whether [].concat(obj) spreads your object |
Symbol.match / Symbol.replace / Symbol.search / Symbol.split / Symbol.match |
All Make your object behave like a RegExp in string methods |
util.inspect.custom (Node.js) |
Customize Node’s REPL and util.inspect() output |
7. Summary
By implementing these methods and symbols you can:
- Control how your objects convert to primitives (Symbol.toPrimitive, valueOf, toString)
- Format locale-sensitive output (toLocaleString)
- Shape JSON payloads (toJSON)
- Label your objects in debug logs (Symbol.toStringTag)
- Enable synchronous or asynchronous iteration (Symbol.iterator, Symbol.asyncIterator)
- Participate in regex operations and instanceof checks (various Symbol.*)
- Integrate smoothly with Node’s inspection tools (util.inspect.custom)
These hooks let your objects feel like first-class citizens of the language. You can start by overriding one or two in your domain classes, see how that improves ergonomics, and gradually adopt more as needed.