I am posting this as a gist because I am too lazy to figure out my blog. TODO: move it to https://domenic.me/ one day.
A lot of specifications use the ToString() abstract operation. (What's an abstract operation?) For example, any web specification which uses Web IDL's DOMString
type (i.e., its basic string type) will convert incoming values using ToString(). Similarly, various parts of the JS specification itself perform ToString(). One example is the Error
constructor, which we will refer to going forward.
If you are trying to implement or polyfill such a specification in JavaScript, how do you do it? For example, given
function ErrorConstructorPolyfill(message) {
if (message !== undefined) {
const msg = toString(message);
// ...
}
// ...
}
then what goes here?
function toString(value) {
return ???;
}
The obvious-but-wrong answer is to use the global String()
function:
function toStringWrong(value) {
return String(value);
}
If you look at the spec for String()
, you'll find that it does ToString() in most cases, but not always. (Spec-reading note: NewTarget will be undefined when called without new
, like we are doing here.) In particular, if the value passed in is a symbol, it will create a stringified representation of the symbol, instead of calling ToString().
To summarize, ToString(), applied to symbols, throws a TypeError
, but String()
returns a descriptive string:
// Throws at the ToString() step
Error(Symbol('foo'));
// Gives "Symbol(foo)"
String(Symbol('foo'));
This means we'd be in bad shape if we used toStringWrong
for our ErrorConstructorPolyfill
:
// Will not throw, but it should!
ErrorConstructorPolyfill(Symbol('foo'));
Instead, the correct thing to do is to use template string interpolation:
function toString(value) {
return `${value}`;
}
This will call directly in to ToString(), and thus if we use this to implement our ErrorConstructorPolyfill
, it will throw, as desired.
TODO: link to meeting notes where we made the decision to make String()
behave like this.