While toString()
should get you what you want 99.9% of the time, going deeper, string conversion can get a little more complicated. There's also the valueOf()
and [Symbol.toPrimitive](hint)
methods. When converting an object to a primitive (e.g. a string), any one of these methods can potentially be called. Which are called depends on their existence, the kind of conversion being performed, and potentially the value they return. Consider the following using both a toString and valueOf:
class MyClass {
toString() {
return 'As string'
}
valueOf() {
return 'As value'
}
}
console.log('' + new MyClass) // As value
console.log(String(new MyClass)) // As string
When using string concatenation (via +
) valueOf()
was used, not toString()
, however toString()
was used with an explicit String()
conversion. If we remove the valueOf()
, we get toString()
used for both.
class MyClass {
toString() {
return 'As string'
}
}
console.log('' + new MyClass) // As string
console.log(String(new MyClass)) // As string
... and this despite objects inheriting a default valueOf()
from Object.prototype. So its not that a valueOf()
doesn't exist here, rather that the default valueOf()
returns this
which is an object and not a primitive so the conversion falls back to using toString()
instead because it demands a primitive value.
const myObj = new MyClass // the version without its own valueOf()
console.log(myObj === myObj.valueOf()) // true
Which method is used first during conversion depends on what you're doing and if the first doesn't return a primitive, the other is used as a fallback. "What you're doing" is divided into 3 kinds of conversions: "string", "number", and "default". More specifically these are "hints" into how the operation is meant to perform the conversion which becomes more clear with the 3rd method [Symbol.toPrimitive](hint)
. If present, this method supersedes all uses of valueOf()
and toString()
(not using them as fallbacks either) but comes with the benefit of having insight into the conversion hint
through its argument.
class MyClass {
[Symbol.toPrimitive](hint) {
console.log(`As primitive with hint: ${hint}`)
return ''
}
}
'' + new MyClass // As primitive with hint: default
String(new MyClass) // As primitive with hint: string
10 * new MyClass // As primitive with hint: number
In the toString()
/valueOf()
the "default" and "number" hints go to valueOf()
first whereas the "string" hint starts with toString()
. When using [Symbol.toPrimitive](hint)
, they all go through that single method passing in the hint along with it. The biggest advantage here being that you can separate "default" from "number" conversions.
So if you want the most control over string and other primitive conversions, you might want to implement a [Symbol.toPrimitive](hint)
. Otherwise, you should get by just fine with a toString()
.
...anyway, theres more than you ever wanted to know about string conversion options for your class.
Bonus trivia: Though Object.prototype doesn't implement [Symbol.toPrimitive](hint)
, some other builtins do, like Date objects. Dates implement one to reverse the precedence of their "default" toString()
and valueOf()
calls. For example where normally a valueOf()
would be called first for a "" + new Date
, instead toString()
is.
const date = new Date
console.log(date.toString()) // Wed Jul 21 2021 14:02:46 GMT-0400 (Eastern Daylight Time)
console.log(date.valueOf()) // 1626890566431
console.log("" + date) // Wed Jul 21 2021 14:02:46 GMT-0400 (Eastern Daylight Time)
// remove toPrimitive for default behavior
Object.defineProperty(date, Symbol.toPrimitive, { value: null });
console.log("" + date) // 1626890566431