Object to primitive conversion

What happends when object are added obj1 + obj2, subtracted obj1 - obj2 or printed using alert(obj)?
There are special methods in objects that do the conversion.
For object, there is no to-boolean conversion, because all objects are true in a boolean context. So there are only string and numeric conversion.
When we output an object like alert(obj), it converts the object to string. String conversion happens usually in this situation.
The numeric conversion happens when we apply mathematical function. For instance, Date object can be suntracted, and the result date1 - date2 is the time difference between two dates.

ToPrimitive

When an object is used in the context where a primitive is required, for instance, in an alert or mathematical operations, it’s converted to a primitive values using the ToPrimiteve algorithm.(specification)
That algorithm allows us to customize the conversion using a special object method.
Depending on the context, the conversion has a so-called “hint”.
There are three variants.

“string”

When an operation expects a string, for object-to-string conversion, like alert:

1
2
// output
alert(obj);

“numeric”

When an operation expects a number, for object-to-number conversion, like maths:

1
2
3
4
5
6
7
8
// explicit conversion
let num = Number(obj);

// maths
let delta = date1 - date2;

// less/greater comparison between 2 objects
let greater = user1 > user2;

“default”

Occurs in rare case when the operator is “not sure” what type to expect.
For instance, binary plus + can work both with strings (concatenates them) and numbers (adds them), so both strings and numbers would do. Or when an object is compared using == with a string, number or a symbol.

1
2
3
4
5
// binary plus
let total = car1 + car2;

// obj == string/number/symbol
if (user == 1) { ... };

The greater/less operator <> can work with both strings and numbers too. Still, it uses “number” hint, not “default”. That’s for historical reasons.

In practice, all built-in objects except for one case(Date object) implement "default" conversion the same way as "number". And probably we should do the same.

To do the conversion, JavaScript tries to call the three object methods:

  1. Call Obj[Symbol.toPrimitive](hint) if the method exists.
  2. Otherwise if hint is "string"
    • try obj.toString() and obj.valueOf(), whatever exists.
  3. Otherwise if hint is "number" or "default"
    • try obj.valueOf() and obj.toString(), whatever exists.

Symbol.toPrimitive

Let’s start from the first method. There’s a built-in symbol named Symbol.toPrimitive(specification) that should be used to name the conversion method, like this:

1
2
3
4
obj[Symbol.toPrimitive] = function(hint) {
// return a primitive value
// hint = one of "string", "number", "default"
}

For instance, here user object implements it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let user = {
name: "John",
money: 1000,

[Symbol.toPrimitive](hint) {
alert(`hint: ${hint}`);
return hint == "string" ? `{name: "${this.name}"}` : this.money;
}
};

// conversions demo:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500

As we can see from the code, the single method user[Symbol.toPrimitive] handles all conversion cases. Depending on the conversion user is converted to a string or a money amount.

toString/valueOf

Methods toString and valueOf provide an alternative “old-style” way to implement the conversion.
JavaScript tries to find those two method in the order:

  • toString -> valueOf for "string" hint.
  • ‘valueOf -> toString’ otherwise.

For instance, user does the same thing as above using a combination of toString and valueOf.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let user = {
name: "John",
money: 1000,

// for hint="string"
toString() {
return `{name: "${this.name}"}`;
},

// for hint="number" or "default"
valueOf() {
return this.money;
}

};

alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500

toPrimitive and toString/valueOf

The important thing to know about all primitive-conversion methods is that they do not necessarily return the “hinted” primitive.
There is no control whether toString() returns exactly a string, or whether Symbol.toPrimitive method returns a number for a hint “number”.

The only mandatory thing: these methods must return a primitive.

For instance:

  • Mathematical operations (except binary plus) perform ToNumber conversion:

    1
    2
    3
    4
    5
    6
    7
    let obj = {
    toString() { // toString handles all conversions in the absence of other methods
    return "2";
    }
    };

    alert(obj * 2); // 4, ToPrimitive gives "2", then it becomes 2
  • Binary plus checks the primitive – if it’s a string, then it does concatenation, otherwise it performs ToNumber and works with numbers.

string example:

1
2
3
4
5
6
7
let obj = {
toString() {
return "2";
}
};

alert(obj + 2); // 22 (ToPrimitive returned string => concatenation)

Number example:

1
2
3
4
5
6
7
let obj = {
toString() {
return true;
}
};

alert(obj + 2); // 3 (ToPrimitive returned boolean, not string => ToNumber)

Summary

The object-to-primitive conversion is called automatically by many built-in functions and operators that expect a primitive as a value.

There are 3 types (hints) of it:

  • "string" (for alert and other string conversion)
  • "number" (for maths)
  • "default" (few operators)

The specification describes explicitly which operator uses which hint. There are very few operators that “don’t know what to expect” and use the "default" hint. Usually for built-in objects "default" hint is handled the same way as "number", so in practice the last two are often merged together.


Reference: JavaScript.info