Object to primitive conversion
Learn what happens when an object meets + or a template literal, which hint JavaScript sends, and how Symbol.toPrimitive, valueOf, and toString control the answer.
- 01Predict the hintSay which hint a piece of code sends to an object:
string,number, ordefault. - 02Follow the orderExplain when JavaScript calls
Symbol.toPrimitive,valueOf, andtoString, and in which order. - 03Build friendly objectsAdd conversion methods so your objects work as you mean them to in logs, math, and template literals.
When an object must become a simple value
In the Type conversion lesson, you saw JavaScript turn values into strings and numbers. You also saw two puzzles. [] + [] gives an empty string. [] + gives the text "[object Object]". This lesson explains the step that was missing.
When JavaScript needs a simple value but has an object, it asks the object to give back one simple value.
A simple value is called a primitive. These are the primitives:
- a string, like
"hi" - a number, like
5 - a boolean:
trueorfalse - a bigint, like
10n - a symbol, a special unique key
nullandundefined
An object is different. It is a container that holds many named values. Some code can't work with a whole container. Here are four examples:
+obj: a plus with nothing on its left. It turns a value into a number.obj * 2: multiplication.`$${obj}`: a template literal. It is text inside backticks, and${...}puts a value into the text.obj == 1: loose equality, with two equals signs. It converts values before it compares them.
Each of these first asks the object for a primitive. Then it carries on with that primitive.
Object to primitive conversion is how JavaScript asks an object for one primitive. It happens before JavaScript turns the value into a string or a number, compares it, or uses it with +.
Start with a small example. The user object below has a toString() method. A toString() method returns the object as text.
const user = { name: "Asha", toString() { return this.name; }}; console.log(String(user));console.log(`${user}`);Line 8 prints Asha. String(user) asks the object for text, so JavaScript calls toString(). Line 9 prints Asha too, because the template literal also asks for text. The object didn't change. Only the code around it changed how JavaScript asked.
When JavaScript asks an object for a primitive, it also says what kind of answer it wants. This word is called the hint. There are three hints: "string", "number", and "default".
Someone hands you a form. A field called "Name" wants text, so you write a name. A field called "Age" wants a number, so you write a number. A field called "Anything" doesn't say, so you write what seems most useful. The field's label is like the hint.
- In real life: A field asks for text
- In JavaScript: The
stringhint - In real life: A field asks for a number
- In JavaScript: The
numberhint - In real life: A field says Anything
- In JavaScript: The
defaulthint - In real life: You write one answer
- In JavaScript: The object returns one primitive
Two later lessons, Type coercion and Equality & sameness, use these rules inside bigger steps. Much later, A tour of the abstract operations shows the names that the specification, the official rulebook for JavaScript, gives them: ToPrimitive and OrdinaryToPrimitive. For now, you only need the rules in this lesson. You can run them and predict them.
The order JavaScript follows
CORE RULEJavaScript doesn't pick a method at random. Each time an object must become a primitive, it follows these steps in order:
- Does the object have a method at the key
Symbol.toPrimitive? If yes, JavaScript calls it and passes the hint:"string","number", or"default". - If that method returns a primitive, JavaScript uses it. If it returns an object, JavaScript throws a
TypeError. ATypeErroris an error that means "this value is the wrong kind". - If there is no such method and the hint is
"string", JavaScript triestoString()first, thenvalueOf(). It uses the first primitive it gets. - If there is no such method and the hint is
"number"or"default", JavaScript triesvalueOf()first, thentoString(). It uses the first primitive it gets. - If none of these gives a primitive, JavaScript throws a
TypeError.
Date objects come with their own built-in Symbol.toPrimitive method. It treats the "default" hint like "string". You'll see this in the section on arrays and dates.
The method at Symbol.toPrimitive must return a primitive. The example below breaks this rule on purpose, so you can spot the error if you meet it later.
const box = { [Symbol.toPrimitive]() { return {}; }}; try { String(box);} catch (error) { console.log(error.name);}- Line 3 returns
{}, an empty object. - Line 8 asks for a string, so JavaScript calls the method and gets an object back.
- JavaScript doesn't go on to try
valueOf()ortoString(). It throws aTypeErrorright away. - The
catchblock on line 9 catches the error. Line 10 prints its name:TypeError.
This is the full error message from a real run of the same code: TypeError: Cannot convert object to primitive value.
Symbol.toPrimitive: the object's own answer
STEP THROUGHA symbol is a special, unique kind of key. Symbol.toPrimitive is a symbol that JavaScript itself knows about. That is why it is called a well-known symbol. When you put a method at this key, the object gives its own answer each time JavaScript asks it for a primitive.
You write the key in square brackets, like [Symbol.toPrimitive](hint) { ... }. The method gets one argument, hint. It is the kind of answer JavaScript wants.
Think of the form again. The usual fields are toString() and valueOf(). Symbol.toPrimitive is a custom answer box at the top of the form. JavaScript reads it first.
- In real life: The form has a custom answer box
- In JavaScript: An object with
[Symbol.toPrimitive] - In real life: The field asks for text or a number
- In JavaScript: The
hintargument - In real life: The box gives one answer
- In JavaScript: The primitive the method returns
The price object below has a method at Symbol.toPrimitive. Step through it, and watch the same object give different answers for different hints.
With a method at Symbol.toPrimitive, one object can give text for one hint and a number for another.
script
cents: 1299, [Symbol.toPrimitive](hint) { if (hint === "string") return "₹12.99"; return this.cents / 100; }}; String(price);+price;price + 1;- Line 4: if the hint is
"string", the method returns the text"₹12.99". - Line 5: for any other hint, it returns
this.cents / 100, which is the number12.99. - Line 9 asks with
"string". Line 10 asks with"number". Line 11 asks with"default", soprice + 1is13.99.
price object, two answers. When JavaScript asks for text, it gets "₹12.99". When it asks for a number, or doesn't say, it gets 12.99.This is powerful, so use it rarely. A money object that works in sums can be handy. A user object that quietly turns into a number is confusing. If the result would surprise someone reading the code, use a named method instead, like price.toNumber().
valueOf and toString: the backup methods
INTERACTIVEMost objects don't have a method at Symbol.toPrimitive. Then JavaScript uses two older methods instead: valueOf() and toString(). This lesson calls them the backup methods. Every plain object already has both. It borrows them from Object.prototype, a built-in object that all plain objects share methods with.
Their names tell you their jobs:
toString()should return the object as text.valueOf()should return the simple value inside the object, if it has one.
The table shows which backup method JavaScript tries first for each hint. Read each row as: "for this hint, try this method, then that one."
| The hint | Tried first | Tried second | Where you see it |
|---|---|---|---|
string | toString() | valueOf() | String(obj) and template literals want text. |
number | valueOf() | toString() | +obj, obj * 2, and obj > 0 want a number. |
default | valueOf() | toString() | obj + 1 and obj == 1 ask without saying which kind they want. |
Now you can explain "[object Object]". A plain object's valueOf() returns the object itself. That is not a primitive, so JavaScript moves on to toString(). A plain object's toString() returns the text "[object Object]". Arrays have their own toString(), so they give different results. You'll see them later in this lesson.
You don't have to use these automatic conversions. When a value could mean more than one thing, named methods are easier to read and to test. In the example below, toFixed(2) turns a number into text with two digits after the dot.
const price = { cents: 500, toLabel() { return `$${(this.cents / 100).toFixed(2)}`; }, toNumber() { return this.cents / 100; }}; console.log(price.toLabel());console.log(price.toNumber() + 2);Line 11 prints $5.00. Line 12 prints 7, because 5 + 2 is 7. The code calls toLabel() when it wants text, and toNumber() when it wants a number. A reader doesn't need to remember which code sends which hint.
Now try the backup methods yourself. Pick which methods the object has. Then step through three conversions and watch which method JavaScript calls. The calls array keeps a list of every method that ran.
Pick which methods the object has. Then step through and see which backup method runs for the string, number, and default hints.
script
const sample = { valueOf() { calls.push("valueOf"); return 42; }, toString() { calls.push("toString"); return "forty-two"; }}; String(sample);+sample;sample + "!";The three hints: string, number, and default
INTERACTIVEThe hint says what kind of answer would help. It doesn't force the final type. The object can return any primitive. After that, the code may convert the primitive again. For example, obj * 2 asks with the "number" hint. If the object returns the text "5", multiplication still turns it into the number 5.
The table lists common code and the hint each one sends. Read each row as: "this code sends this hint, for this reason."
| When you write | The hint sent | Why |
|---|---|---|
String(obj) | string | You asked for a string directly. |
A template, ${obj} | string | A template literal builds text. |
[obj].join() | string | join() turns each item in the array into text. |
+obj | number | A plus with nothing on its left only turns a value into a number. |
obj * 2 | number | Multiplication only works with numbers. |
obj > 0 | number | Comparing with > or < asks objects for numbers first. |
obj + 1 | default | A plus between two values can add numbers or join text, so it doesn't say which it wants. |
obj == 1 | default | Loose equality asks the object for a primitive when the other side is a primitive. |
String(obj) wants text, +obj wants a number, and obj + 1 doesn't say yet.Try the code yourself. The object below prints the hint it gets. Then it returns a primitive that makes the result easy to read:
"object-card"for the"string"hint7for the"number"hint3for the"default"hint
const obj = { [Symbol.toPrimitive](hint) { console.log("hint:", hint); if (hint === "string") return "object-card"; if (hint === "number") return 7; return 3; }}; String(obj);`${obj}`;obj + "";+obj;obj * 2;obj + 1;obj == 1;obj > 0;[obj].join();You picked String(obj). The object got the string hint, and the result is "object-card".
String(obj)`${obj}`[obj].join()+objobj * 2obj > 0obj + 1obj == 1
Sort each piece of code by the hint it sends to the object.
Arrays, objects, and dates explain the old puzzles
REAL OUTPUTSArrays are objects too. An array's toString() works like join(","). It puts the items together into one piece of text, with a comma between them. This one fact explains many JavaScript puzzles.
String({});String([]);[1, 2] + "";[5] * 2;[1, 2] + [3];[] + {};({} + []);const date = new Date("2020-01-02T00:00:00.000Z");String(date);+date;date + 1;In the arrays example, line by line:
- Line 1:
String({})is"[object Object]", from a plain object'stoString(). - Line 2:
String([])is an empty string. An empty array has no items to join. - Line 3:
[1, 2] + ""becomes"1,2". - Line 4:
[5] * 2first turns the array into"5". Multiplication then turns"5"into 5, so the result is10. - Line 5:
[1, 2] + [3]becomes"1,2" + "3". Both sides are text, so+joins them into"1,23". - Line 6:
[] + {}becomes"" + "[object Object]", which is"[object Object]". - Line 7:
({} + [])is also"[object Object]". The round brackets matter here. In the console, a line that starts with{}can be read as an empty block of code, not an object. Then only+[]is left, which is0. The brackets make JavaScript read{}as an object.
+ sees two strings, so it joins them: "1,2" + "3" is "1,23".Dates are different. They come with their own Symbol.toPrimitive method. In the date example, line by line:
- Line 2:
String(date)gives the date as readable text. - Line 3:
+dateasks with the"number"hint. It gives a timestamp: the number of milliseconds since 1 January 1970. - Line 4:
date + 1asks with the"default"hint. A date treats"default"like"string", so it returns the date as text. Then+adds"1"to the end of that text.
Where you’ll use this
Most objects in an app don't need their own conversion. Named methods are usually clearer. But knowing how conversion works helps in three places:
- Readable logs and messages. A good
toString()means your logs show useful text, not"[object Object]". - Small value objects. A value object is a small object that stands for one value, such as an amount of money, a length, a temperature, or an ID. Some of these can have a simple, well-tested conversion.
- Finding bugs. When
+,==, or>gives a strange result, ask "which hint did this send?" instead of guessing.
A conversion method is your own code, and JavaScript may call it when you don't expect it. For example, logging and template literals call it. So keep these methods short and safe:
- They should only build and return a value.
- They shouldn't change anything, such as the items in a cart.
- They shouldn't send requests over the network.
- They should give the same answer every time they are called.
Use named methods for your app's main logic. Use automatic conversion only where the meaning is obvious: log messages, labels, and tiny value objects.
A total shown on screen is a good place for automatic conversion. In the example below, the total turns into readable text inside a message. The number of cents stays in its own property, ready for math and for saving.
const cartTotal = { cents: 1299, toString() { return `$${(this.cents / 100).toFixed(2)}`; }}; console.log(`Total: ${cartTotal}`);console.log(cartTotal.cents / 100);Line 8 prints Total: $12.99. Line 9 prints 12.99. The toString() method only builds the label. The math on line 9 uses the cents property directly.
Not every piece of code asks for a primitive. This explains why a conversion method sometimes seems to be skipped. Here are two examples:
- Truthiness is whether a value counts as true in a check like
if. Every object counts as true. JavaScript doesn't ask the object for a primitive to decide this. JSON.stringify()turns an object into JSON text. It reads the object's own properties. It doesn't calltoString().
const amount = { valueOf() { return 0; }}; console.log(Boolean(amount));console.log(amount == 0);const user = { name: "Asha", toString() { return "not used by JSON"; }}; console.log(JSON.stringify(user));- Boolean example, line 7:
Boolean(amount)printstrue, because every object counts as true. - Boolean example, line 8:
amount == 0asks the object for a primitive.valueOf()returns 0, and 0 equals 0, so it printstrue. - JSON example, line 8: it prints
{"name":"Asha"}.JSON.stringify()read thenameproperty and didn't calltoString().
Common misconceptions
- "A plus sign asks for a string." Not at first. With an object,
+sends the"default"hint. Only when both sides are primitives does it choose: if either side is text, it joins them; if not, it adds them. - "The hint decides the final type." No. The object can return any primitive. The code may convert that primitive again afterward.
- "valueOf() always runs first." Only for the
"number"and"default"hints. The"string"hint triestoString()first. - "Plain objects can't become strings." They can. Their built-in
toString()returns"[object Object]". - "Dates work like plain objects." No. A date treats the
"default"hint like"string". That is whydate + 1adds"1"to the end of the date text. - "If Symbol.toPrimitive returns an object, JavaScript tries valueOf() next." No. It throws a
TypeError. The method must return a primitive. - "A conversion method can do any work." It shouldn't. It should only return a value. Don't change data inside it, such as the items in a cart.
- "Every value object needs Symbol.toPrimitive." No. Start with named methods. Add automatic conversion only when the meaning is obvious and won't change.
Practice exercises
5 EXERCISESWhich hint does JavaScript send to the object in obj + 1?
obj + 1 sends the default hint. Once both sides are primitives, + decides whether to add or to join text.
Symbol.toPrimitiveAdd a method so that String(price) is "₹25.00" and price + 5 prints 30. Then type the second printed value.
const price = {
cents: 2500,
// add Symbol.toPrimitive here
};
console.log(String(price));
console.log(price + 5);const price = {
cents: 2500,
[Symbol.toPrimitive](hint) {
if (hint === "string") return "₹25.00";
return this.cents / 100;
}
};
console.log(String(price));
console.log(price + 5);For the string hint, the method returns the label. price + 5 sends the default hint, so the method returns 25, and 25 + 5 prints 30.
Predict what both lines print. Type the first result, then the second, in the answer box.
console.log([5] * 2);
console.log([1, 2] + [3]);[5] * 2 prints 10. [1, 2] + [3] becomes "1,2" + "3", so it prints 1,23.
[object Object] in a templateThe starter code prints Hello, [object Object]!. Add a method so the template prints the user's name.
const user = { name: "Ada" };
console.log(`Hello, ${user}!`);const user = {
name: "Ada",
toString() {
return this.name;
}
};
console.log(`Hello, ${user}!`);The new toString() returns the name. The template literal uses it, so the code prints Hello, Ada!.
date + 1In a few words: does date + 1 join text (string concatenation) or add numbers (numeric addition)?
date + 1 sends the default hint. A date treats it like string, so the date becomes text, and 1 is added to the end of that text. +date would send number and give a timestamp.
Check your understanding
8 QUESTIONSQuestion 1 of 8An object has a method at
Symbol.toPrimitive. What does JavaScript do first?Choose an answer to see the explanation.
Question 2 of 8What does this object plus 1 print?
Read the code, then predictconst obj = { valueOf() { return 4; } }; console.log(obj + 1);Choose an answer to see the explanation.
Question 3 of 8Which piece of code sends the
stringhint?Choose an answer to see the explanation.
Question 4 of 8What does this array times 2 print?
Read the code, then predictconsole.log([5] * 2);Choose an answer to see the explanation.
Question 5 of 8What does this array plus another array print?
Read the code, then predictconsole.log([1, 2] + [3]);Choose an answer to see the explanation.
Question 6 of 8What happens if the method at
Symbol.toPrimitivereturns{}?Choose an answer to see the explanation.
Question 7 of 8Why is
date + 1usually text, while+dateis a timestamp?Choose an answer to see the explanation.
Question 8 of 8What does this line print?
Read the code, then predictconst amount = { valueOf() { return 0; } }; console.log(Boolean(amount), amount == 0);Choose an answer to see the explanation.
Key takeaways
- JavaScript checks for a method at
Symbol.toPrimitivefirst, and passes it the hint. - If that method returns an object, JavaScript throws a
TypeError. - Without that method, the
"string"hint triestoString()beforevalueOf(). - Without that method, the
"number"and"default"hints tryvalueOf()beforetoString(). - A plus between two values and
==send"default".+obj,*, and>send"number". Template literals andString()send"string". - Arrays turn into text by joining their items with commas. Dates treat the
"default"hint like"string".
Object to primitive conversion is JavaScript asking an object for one primitive, using a hint, before the rest of the code goes on.
Up next: Array basics.