valueOf() vs toString() in JavaScript: Why Does valueOf() Trump String Conversion?

JavaScript is a dynamically typed language, meaning variables can hold values of any type, and type conversion happens implicitly all the time. When working with objects, JavaScript often needs to convert them to primitive values (like numbers or strings) for operations like arithmetic, concatenation, or comparison. Two critical methods drive this conversion: valueOf() and toString().

At first glance, toString() might seem more intuitive—it returns a string representation of an object, which is commonly used for logging or display. But in many cases, valueOf() takes precedence, "trumping" toString() during implicit conversion. Why is that?

This blog dives deep into valueOf() and toString(), how JavaScript’s internal conversion logic works, and why valueOf() often wins when objects are converted to primitives. By the end, you’ll understand when and why each method is called, and how to predict (and control) object-to-primitive conversion in your code.

Table of Contents#

  1. Understanding valueOf()
  2. Understanding toString()
  3. JavaScript’s ToPrimitive Operation: The Hidden Conversion Logic
  4. valueOf() vs toString(): Battle of the Conversion Methods
  5. What About Symbol.toPrimitive?
  6. Best Practices for Custom Objects
  7. Conclusion
  8. References

1. Understanding valueOf()#

The valueOf() method is a built-in function available on all JavaScript objects. Its primary purpose is to return the primitive value of an object. A primitive value is a non-object value like number, string, boolean, null, undefined, or symbol.

How It Works:#

  • For built-in objects (e.g., Number, String, Boolean), valueOf() returns the underlying primitive value.
  • For custom objects, valueOf() defaults to returning the object itself (not a primitive), unless explicitly overridden.

Examples of valueOf():#

// Number object: returns the primitive number
const numObj = new Number(42);
console.log(numObj.valueOf()); // 42 (primitive number)
 
// String object: returns the primitive string
const strObj = new String("hello");
console.log(strObj.valueOf()); // "hello" (primitive string)
 
// Boolean object: returns the primitive boolean
const boolObj = new Boolean(true);
console.log(boolObj.valueOf()); // true (primitive boolean)
 
// Array: returns the array itself (not a primitive)
const arr = [1, 2, 3];
console.log(arr.valueOf() === arr); // true (array is an object)
 
// Plain object: returns the object itself
const obj = { name: "Alice" };
console.log(obj.valueOf() === obj); // true (object reference)

2. Understanding toString()#

The toString() method, also built into all objects, returns a string representation of the object. Unlike valueOf(), its output is always a string, even for primitives (since primitives are wrapped in object wrappers when methods are called).

How It Works:#

  • For built-in objects, toString() returns a human-readable string (e.g., [1, 2].toString()"1,2").
  • For plain objects ({}), the default toString() returns "[object Object]".
  • Custom objects can override toString() to return custom string representations.

Examples of toString():#

// Number object: returns string "42"
const numObj = new Number(42);
console.log(numObj.toString()); // "42"
 
// Array: returns comma-separated string
const arr = [1, 2, 3];
console.log(arr.toString()); // "1,2,3"
 
// Plain object: default string representation
const obj = { name: "Alice" };
console.log(obj.toString()); // "[object Object]"
 
// Custom object with overridden toString()
const person = {
  name: "Bob",
  age: 30,
  toString() {
    return `${this.name}, ${this.age} years old`;
  }
};
console.log(person.toString()); // "Bob, 30 years old"

3. JavaScript’s ToPrimitive Operation: The Hidden Conversion Logic#

To understand why valueOf() often trumps toString(), we need to explore JavaScript’s internal ToPrimitive abstract operation. This operation is called whenever JavaScript needs to convert an object to a primitive value (e.g., during arithmetic, concatenation, or comparison).

What is ToPrimitive?#

ToPrimitive is an abstract operation (not directly callable in code) that takes two arguments:

  • input: The object to convert.
  • hint: An optional "preferred type" for the result, which can be:
    • "number": Prefer a numeric primitive.
    • "string": Prefer a string primitive.
    • "default": No preference (used in ambiguous cases like the + operator).

Conversion Hints: 'number', 'string', and 'default'#

The hint determines the order in which valueOf() and toString() are called to get a primitive value:

HintConversion Order
"number"1. Call input.valueOf(). If the result is a primitive, return it.
2. If not, call input.toString(). If the result is a primitive, return it.
3. If neither returns a primitive, throw a TypeError.
"string"1. Call input.toString(). If the result is a primitive, return it.
2. If not, call input.valueOf(). If the result is a primitive, return it.
3. If neither returns a primitive, throw a TypeError.
"default"Behaves like "number" for most objects (e.g., Array, plain objects).
Exception: Date objects use "string" logic for "default" hints.

When Are Hints Used?#

The hint is determined by the context in which the object is used. Here are common scenarios:

ScenarioHint UsedExample
Numeric operations (+, -, *, /, >, <)"number"obj + 5, obj > 10
Number(obj) constructor"number"Number({ valueOf: () => 42 })
String operations (String(obj), template literals)"string"String({ toString: () => "hello" }), `${obj}`
+ operator (ambiguous: addition or concatenation)"default"obj + "test", obj + 5 (if no string operand)
Date objects in + or =="default" (uses "string" logic)new Date() + " today"

4. valueOf() vs toString(): Battle of the Conversion Methods#

Now that we understand ToPrimitive and hints, let’s see why valueOf() often "trumps" toString() in practice.

Scenario 1: Numeric Conversion (e.g., Number(obj), Arithmetic)#

For numeric operations, JavaScript uses the "number" hint. This means valueOf() is called first. Only if valueOf() returns a non-primitive (e.g., an object) does toString() get a chance.

Example 1: valueOf() Returns a Primitive#

const customObj = {
  valueOf: () => 100, // Returns a primitive number
  toString: () => "200" // Returns a string (ignored here)
};
 
// Number() uses "number" hint: valueOf() is called first
console.log(Number(customObj)); // 100 (from valueOf())
 
// Arithmetic operation (+) with numeric hint: valueOf() wins
console.log(customObj + 50); // 150 (100 + 50)

Example 2: valueOf() Returns an Object (Falls Back to toString())#

If valueOf() returns an object (non-primitive), ToPrimitive falls back to toString():

const customObj = {
  valueOf: () => {}, // Returns an object (non-primitive)
  toString: () => "200" // Returns a string primitive
};
 
console.log(Number(customObj)); // 200 (from toString())
console.log(customObj + 50); // "20050" (string concatenation? Wait, why?)

Wait—why is customObj + 50 "20050" here? Because valueOf() returns an object, so toString() is called, returning "200" (a string). Now + sees a string operand, so it concatenates: "200" + 50 → "20050".

Scenario 2: String Conversion (e.g., String(obj), Template Literals)#

For string-specific operations, JavaScript uses the "string" hint, so toString() is called first.

Example:#

const customObj = {
  toString: () => "hello", // Returns a string primitive
  valueOf: () => 123 // Returns a number (ignored here)
};
 
// String() uses "string" hint: toString() is called first
console.log(String(customObj)); // "hello"
 
// Template literal uses "string" hint: toString() wins
console.log(`Message: ${customObj}`); // "Message: hello"

Scenario 3: The Tricky + Operator#

The + operator is ambiguous: it can perform numeric addition or string concatenation. For objects, + triggers ToPrimitive with the "default" hint.

  • For most objects (e.g., plain objects, arrays), "default" hint uses "number" logic: valueOf() first, then toString().
  • For Date objects, "default" hint uses "string" logic: toString() first (see next section).

Example: + with Non-Date Objects#

const obj = {
  valueOf: () => 42,
  toString: () => "42"
};
 
// + uses "default" hint → "number" logic → valueOf() is called
console.log(obj + obj); // 84 (42 + 42, numeric addition)

Edge Case: Date Objects (When toString() Takes Over)#

Date objects are special: they use "string" logic for "default" hints. This means toString() is called first during + operations, even though valueOf() exists.

Date.prototype.valueOf() returns the timestamp (milliseconds since Unix epoch), while Date.prototype.toString() returns a human-readable date string.

Example: Date and the + Operator#

const date = new Date("2023-01-01");
 
// Date's valueOf() returns timestamp (number)
console.log(date.valueOf()); // 1672531200000
 
// Date's toString() returns human-readable string
console.log(date.toString()); // "Sun Jan 01 2023 00:00:00 GMT+..."
 
// + operator uses "default" hint → Date uses "string" logic → toString()
console.log(date + " was a great day!"); 
// "Sun Jan 01 2023 00:00:00 GMT+... was a great day!" (string concatenation)
 
// But numeric operations use "number" hint → valueOf()
console.log(date - 86400000); // 1672444800000 (timestamp for 2022-12-31)

5. What About Symbol.toPrimitive?#

There’s a third player in the game: Symbol.toPrimitive. Introduced in ES6, this symbol property allows objects to explicitly define how they convert to primitives, overriding both valueOf() and toString().

How It Works:#

If an object has a Symbol.toPrimitive method, ToPrimitive calls it directly, passing the hint as an argument. The method can then return a primitive based on the hint.

Example:#

const obj = {
  [Symbol.toPrimitive](hint) {
    if (hint === "number") return 100;
    if (hint === "string") return "hello";
    return "default"; // for "default" hint
  },
  valueOf: () => 999, // Ignored!
  toString: () => "oops" // Ignored!
};
 
console.log(Number(obj)); // 100 (hint: "number")
console.log(String(obj)); // "hello" (hint: "string")
console.log(obj + " world"); // "default world" (hint: "default")

6. Best Practices for Custom Objects#

When defining custom objects, override valueOf() and toString() thoughtfully:

  1. valueOf(): Return a numeric primitive if the object represents a quantity (e.g., a Currency object).
  2. toString(): Return a human-readable string (e.g., "$42.99" for Currency).
  3. Avoid non-primitive returns: Never return an object from valueOf() or toString() unless you intend to fall back to the other method.
  4. Use Symbol.toPrimitive for full control: If you need granular control over conversion (e.g., different behavior for "number" vs. "string" hints), use Symbol.toPrimitive.

7. Conclusion#

valueOf() and toString() are both critical for object-to-primitive conversion, but their priority depends on JavaScript’s ToPrimitive operation and the conversion hint:

  • valueOf() trumps toString() in most cases (e.g., numeric operations, + with non-Date objects) because the "number" and "default" hints prioritize valueOf().
  • toString() takes over only when the hint is "string" (e.g., String(obj), template literals) or for Date objects with "default" hints.

Understanding these rules helps you predict and control how your objects behave in implicit conversion scenarios—saving you from head-scratching bugs!

8. References#