# JS Tip of the Day: String Conversions in Depth

**URL:** <https://forum.kirupa.com/t/js-tip-of-the-day-string-conversions-in-depth/643047>\
**Category:** web dev\
**Created:** [June 1, 2020, 1:51pm UTC](https://forum.kirupa.com/t/js-tip-of-the-day-string-conversions-in-depth/643047 "2020-06-01T13:51:15Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![senocular](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/senocular/32/7217_2.png) [@senocular](https://forum.kirupa.com/u/senocular)\
**Post date:** [June 1, 2020, 1:51pm UTC](https://forum.kirupa.com/t/js-tip-of-the-day-string-conversions-in-depth/643047/1 "2020-06-01T13:51:15Z")

</div>

**String Conversions in Depth**  
Version: ES2015 (Symbol, Symbol.toPrimitive)  
Level: Advanced

Converting values to strings is such a trivial thing, it’s easy to overlook the complexities of it. That is, maybe, at least until it becomes a problem, like when you start seeing `[object Object]` in unexpected places. We’ve seen how that can be customized with `Symbol.toStringTag`, but that’s only part of the string conversion story.

When it comes to converting values in JavaScript to strings it’s all about first reducing the value into a primitive value if it isn’t one already. Once you have a primitive value, the conversion from that value to a string follows a (mostly) simple set of rules:

1. If the primitive is already a string, return the string
2. If the primitive is `undefined` return “undefined”
3. If the primitive is `null` return “null”
4. If the primitive is `true` return “true”
5. If the primitive is `false` return “false”
6. If the primitive is a number (Number or BigInt) return the string version of the number\*
7. If the primitive is a symbol throw an error

> \* The process for string conversion from numbers is a little complex, dealing with special values like `Infinity` and `NaN` and different ways to present very large or small numbers, so I won’t get into the details here, but for the most part it’s not doing anything too terribly unexpected.

Symbols are a bit of an anomaly here. You may have noticed that they will throw an error when being converted to a string. Symbols can, in fact, be converted to strings, and normally that step would have looked something like:

1. If the primitive is a symbol return `Symbol(${primitive.description ?? ''})`

But the actual error-throwing behavior was added to prevent people from accidentally converting a symbol key in to a string key without realizing it. To get the string conversion behavior above, you must explicitly convert the symbol to a string by passing it into `String()` or calling its `toString()` method.

```javascript
let privateKey = Symbol('private');
let badMessage = `The key is ${privateKey}.`;
// TypeError: cannot convert (implicit)

let workingMessage = `The key is ${String(privateKey)}.`;
// (Ok, explicit)
console.log(workingMessage); // The key is Symbol(private).

```

Overall, `String()` is generally preferred over the `toString()` method since it can work on values that do not have `toString()` methods such as `undefined` and `null`. `String()` also makes a special exception for symbols, converting them to strings rather than going through the steps that would normally throw an error.

When it comes to objects being converted to strings, it would be easy to assume that it would always be handled through an object’s `toString()` method. But in reality, that’s not always the case. Since objects are converted to primitives and not directly to strings when they’re converted into strings, it’s possible another method might be used. There are, in fact, 3 possible approaches for converting objects into primitives. They include:

- `toString()`
- `valueOf()`
- `Symbol.toPrimitive`

What approach is used depends on availability and why or how the object is getting converted to a primitive in the first place. Causes for an object to be converted into a primitive may include:

- Primitive conversions/factories (BigInt, Number, String, Symbol)
- Conversion to object property key
- Arithmetic or bitwise operations
- String concatenation
- Loose equality comparisons

Before getting into those, let’s take a look at the different to primitive approaches, starting first with `toString()`. This is the method that, given the default implementation provided by the Object type, produces the infamous “[object Object]” string. Other object types may instead provide alternate implementations of `toString()`. For example arrays will output something similar to calling `join(',')` while RegExp objects will produce something matching their literal form. Most objects, however, will be using the default Object implementation.

```javascript
let ordinaryObj = {};
console.log(ordinaryObj.toString()); // [object Object]

let numbersArr = [1,2,3];
console.log(numbersArr.toString()); // 1,2,3

let regex = /abc/gi;
console.log(regex.toString()); // /abc/gi

```

`valueOf()`, on the other hand, is used to get the “value” of the current object. This usually means the object itself, as the default implementation by Object returns. The primitive object types have their own implementation, though, using `valueOf()` as a way to allow you to access to their primitive values. For example an object version of a number would return its primitive number value when calling `valueOf()` rather than the number object itself.

```javascript
let ordinaryObj = {};
console.log(ordinaryObj.valueOf() === ordinaryObj); // true

let numberObj = new Number(3);
console.log(numberObj.valueOf() === numberObj); // false
console.log(numberObj.valueOf() === 3); // true

```

`Symbol.toPrimitive` is a little different. Object does not implement this, nor do most other built-in types with the exceptions of Symbol and Date. Instead `Symbol.toPrimitive` represents an override to both `toString()` and `valueOf()` to be used as an alternative to those methods when an object needs to be converted to a primitive. To understand how this is used, we need to get back to the reasons for a conversion.

Ignoring `Symbol.toPrimitive` for a moment, the standard approach taken to convert an object to a primitive would be to use either `toString()` or `valueOf()`. If only one exists, that one will be used. If they both exist, one is preferred, and if when called, it doesn’t return a primitive, the other is used. If neither exist, or neither of those available return a primitive, an error is thrown. Which one is called first depends on the operation converting the object to a primitive.

- Prefers `toString()`:
  - Primitive conversions/factories (String, Symbol)
  - Conversion to object property key

- Prefers `valueOf()`:
  - Primitive conversions/factories (BigInt, Number)
  - Arithmetic or bitwise operations
  - String concatenation
  - Loose equality comparisons

There are actually 3 kinds of preferences: “string” (`toString()`), “number” (`valueOf()`), and “default”. These are known as the “hint” in the conversion process. But by default, the “default” hint will use `valueOf()`.

Of the conversions listed above under `valueOf()`, both string concatenation and loose equality comparisons have a hint of “default” (yes, you heard right, for whatever reason string concatenation uses “default”, not “string”). While this distinction isn’t made by `valueOf()` it is seen in `Symbol.toPrimitive` as the hint gets passed in as an argument.

Taking these preferences into account, and including the `Symbol.toPrimitive` override, we can map out the rules for object to primitive conversions:

1. If the object has a `Symbol.toPrimitive` method, call it, passing in hint, capturing its return value as `result`
  1. If `result` is a primitive return `result`
  2. Else throw an error

2. If the operation has a “string” hint, set _primary_ to `toString()` and set _secondary_ to `valueOf()`
3. Else set _primary_ to `valueOf()` and set _secondary_ to `toString()`
4. If the object has a _primary_ method, call it capturing its return value as `result`
  1. If `result` is a primitive return `result`

5. If the object has a _secondary_ method, call it capturing its return value as `result`
  1. If `result` is a primitive return `result`

6. If no primitive has been returned by this point, throw an error

One thing to notice is that a `Symbol.toPrimitive` method overrides the other methods completely. If it exists at all, the others are ignored and never called. If it fails to return a primitive, an error is thrown. Since in most cases this method won’t exist, it will normally come down to `toString()` and `valueOf()`.

Of course because most object implementations of `valueOf()` use the default from Object which returns the object itself, chances are, even if `valueOf()` is preferred and called first, the non-primitive object value it returns is going to cause it to get ignored. So in the end, when objects are converted to primitives, `toString()` ends up being the most likely method used.

We can see this conversion at work by creating a spy object with each method implemented and running it through some conversions to see what approaches are taken.

```javascript
let spy = {
    toString () {
        console.log('toString()');
        return 'spy';
    },
    valueOf () {
        console.log('valueOf()');
        return 'spy';
    },
    [Symbol.toPrimitive] (hint) {
        console.log(`toPrimitive(${hint})`);
        return 'spy';
    }
};

```

You may have already realized that simply because `Symbol.toPrimitive` exists, it will be used for all of the primitive conversions and `toString()` and `valueOf()` will be ignored. By capturing the `hint` passed into it when called, we can also see the preference for each kind of conversion.

```javascript
String(spy);
// toPrimitive(string)

let object = {};
object[spy] = 'as a key';
// toPrimitive(string)

Number(spy);
// toPrimitive(number)

spy * 3;
// toPrimitive(number)

spy + 'concat';
// toPrimitive(default)

spy == 'compared';
// toPrimitive(default)

```

Next we can remove `Symbol.toPrimitive` to see `toString()` and `valueOf()` getting called. These should match up with the preferences seen in the previous calls to the `Symbol.toPrimitive` method, where “string” hints call `toString()`, and “number” and “default” call `valueOf()`.

```javascript
let spy = {
    toString () {
        console.log('toString()');
        return 'spy';
    },
    valueOf () {
        console.log('valueOf()');
        return 'spy';
    }
};

String(spy);
// toString()

let object = {};
object[spy] = 'as a key';
// toString()

Number(spy);
// valueOf()

spy * 3;
// valueOf()

spy + 'concat';
// valueOf()

spy == 'compared';
// valueOf()

```

Finally we can prevent either from returning a primitive to see how each `toString()` and `valueOf()` is used as a fallback for the other. However, since neither return a primitive, each conversion will also fail catastrophically with a TypeError.

```javascript
let spy = {
    toString () {
        console.log('toString()');
        return this;
    },
    valueOf () {
        console.log('valueOf()');
        return this;
    }
};

String(spy);
/* logs:
toString()
valueOf()
(then TypeError!)
*/

let object = {};
object[spy] = 'as a key';
/* logs:
toString()
valueOf()
(then TypeError!)
*/

Number(spy);
/* logs:
valueOf()
toString()
(then TypeError!)
*/

spy * 3;
/* logs:
valueOf()
toString()
(then TypeError!)
*/

spy + 'concat';
/* logs:
valueOf()
toString()
(then TypeError!)
*/

spy == 'compared';
/* logs:
valueOf()
toString()
(then TypeError!)
*/

```

While most objects will be using `toString()`, or maybe even `valueOf()` in these conversions, if you’ll remember, there were two builtins that implemented the `Symbol.toPrimitive` method. These were Symbol and Date. The reasons for them doing so are different for each.

One of the things that makes symbols unique is that they can be used as property keys in objects just like strings. However, when it comes to converting objects to property keys, you may have noticed that `toString()` is preferred over `valueOf()`. This could cause problems for symbol objects since their symbol values should be used as keys, not their string representations. To fix this, Symbol implements its own `Symbol.toPrimitive` method to return a symbol object’s symbol value rather than its string value for any primitive conversion.

```javascript
let symbolPrim = Symbol('key');
let symbolObj = Object(symbolPrim);

let rightKey = {};
rightKey[symbolObj] = 'as a key';
console.log(rightKey[symbolPrim]); // as a key

// show would-be default behavior without override
Object.defineProperty(symbolObj, Symbol.toPrimitive, { value: null });

let wrongKey = {};
wrongKey[symbolObj] = 'as a key';
console.log(wrongKey[symbolPrim]); // undefined
console.log(wrongKey['Symbol(key)']); // as a key

```

Date objects on the other hand are unique in that they have a “valueOf” value that’s not the date object itself. A date’s value is the same as it’s `getTime()` value which is a primitive number. Because of this, primitive conversions for date objects that would start with `valueOf()` would also end with `valueOf()`. This is especially troublesome for dates when dealing string concatenation which prefers `valueOf()` to `toString()`.

To counter this, the Date type implements a `Symbol.toPrimitive` method which swaps the ordering of `valueOf()` and `toString()` for conversions with the “default” hint.

```javascript
let dateObj = new Date(1979, 0);
console.log(dateObj.valueOf()); // 284014800000
console.log(dateObj.toString());
// Mon Jan 01 1979 00:00:00 GMT-0500 (Eastern Standard Time)
console.log(dateObj + '');
// Mon Jan 01 1979 00:00:00 GMT-0500 (Eastern Standard Time)

// show would-be default behavior without override
Object.defineProperty(dateObj, Symbol.toPrimitive, { value: null });

console.log(dateObj + ''); // 284014800000

```

After all is said and done, most of the time, for most objects, the path to becoming a string hinges on `toString()`. For your own objects, if you want to tweak the default implementation of `toString()` (as provided by Object), you can do that with `Symbol.toStringTag`. Otherwise, you can implement your own, custom `toString()` for something entirely different. As long as you’re not also implementing a `valueOf()` or working from a primitive object type (which is unlikely), that’s all there is to it.

For more complicated and controlled conversions you can use `Symbol.toPrimitive`. With the `hint` argument, you’ll even be able to distinguish between 3 different kinds of conversions - more than what’s possible with `toString()` and `valueOf()` alone. And even though a `String.toPrimitive` implementation would supersede `toString()` and `valueOf()`, they’re still good to implement in case anyone would want to call them directly.

```javascript
class Chameleon {
    constructor (str, num, def) {
        this.str = str;
        this.num = num;
        this.def = def;
    }
    toString () {
        return this.str;
    }
    valueOf () {
        return this.num;
    }
    [Symbol.toPrimitive] (hint) {
        switch (hint) {
            case "string":
                return this.toString();
            case "number":
                return this.valueOf();
            default:
                return this.def;
        }
    }
}

```

```javascript
let geeko = new Chameleon('SUSE', 15.1, 'Linux');
console.log(geeko == 'Linux'); // true

console.log('Major release: ' + Math.round(geeko));
// Major release: 15

let distros = {
  [geeko]: true
};
console.log(Object.keys(distros)); // ["SUSE"]

```

More info:

- [String on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)
- [Object toString on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString)
- [Object valueOf on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf)
- [Symbol.toPrimitive on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive)

* * *

More tips: [JavaScript Tips of the Day](http://forum.kirupa.com/t/javascript-tips-of-the-day/641803/1)
