awesome-badger

toString || !toString

toString, or not toString? That is the question—
Whether ‘tis nobler in the mind to suffer
the slings and arrows of outrageous type conversion,
Or to take up arms against a sea of troublesome JavaScript behaviours,
and by opposing end them? To Git, to blame—
No more—and by a pull request to say we end
The heartache and the thousand natural shocks
That programmers are heir to—’tis a consummation
Devoutly to be wished!…

(with apologies to the Bard)

Disclosure

This little blog post was developed out of a tweet by Mike Bostock (of D3 and Observable fame) about inconsistent JavaScript behaviour.

Why is toString the Question?

Let’s say you have created some custom object and you know that at some point, you will need the string representation of that object.

Ok, fine. Let’s give the object an explicit toString function:

let someObj = { toString: () => "Blah" }

So if we now put that object into a template literal, everything behaves as expected:

`${someObj}`    // 'Blah'

Ok, that’s fine.

Another way to perform string conversion is to overload the + operator.

someObj + ""    // 'Blah'

Fair enough; but this is JavaScript, so we should expect some inconsistencies…

And Now, Without the toString Function

Let’s say that our custom object no longer has a toString function, but instead, has a valueOf function.

let someObj = { valueOf: () => "Surprise!" }

Again, let’s try our two approaches for performing string conversion. First, let’s use a template literal:

`${someObj}`    // '[object Object]'  Uhhh, what!?

Hmmm, that didn’t work. Let’s try overloading the + operator:

someObj + ""    // 'Surprise!'  OK, that's better

The problem here is that the template literal specifically calls an object’s toString function, but if that function is missing, it simply gives up and prints [object Object]

The overloaded + operator on the other hand, appears to see that the object does not contain a toString function, and instead of giving up, calls the next best function – valueOf (then converts whatever value it receives into a string).

Ok, so template literals and the overloaded + operator both perform string conversion, but according to different rules…

Choosing Between toString and valueOf

Let’s now take the perfectly reasonable step of giving our custom object both a toString function and a valueOf function…

let someObj = { toString: () => "Blah", valueOf: () => "Surprise!" }
`${someObj}    // 'Blah'         Yup, that's what we want
someObj + ""   // 'Surprise!'    Huh?! Why wasn't toString() called?

Think

Strange, but Consistent

So, although its a bit weird, we have established a pattern for how string conversion is performed when using either template literals or the overloaded + operator:

Template literals

Does the object have a toString function?

The overloaded + operator

During string conversion, the overloaded + operator prioritises the valueOf function over the toString function (and no, I can’t explain why), so:

Does the object have a valueOf function?

Strange, but there you have it.

Consistently Inconsistent

Let’s apply what we’ve learnt about string conversion to the good old Date object:

date = new Date()
date.valueOf()      // 1632481716606
date.toString()     // 'Fri Sep 24 2021 12:08:36 GMT+0100 (British Summer Time)'

Yup, that’s all pretty normal.

So based on the above pattern of behaviour, converting the date object to a string using a template literal will cause the toString function to be called:

`${date}`           // 'Fri Sep 24 2021 12:08:36 GMT+0100 (British Summer Time)'

Nice!

And converting the date object to a string by overloading the + operator will cause the valueOf function to be called:

date + ""           // 'Fri Sep 24 2021 12:08:36 GMT+0100 (British Summer Time)'

WAT