TLDR
Type coercion, or implicit type conversion, enables weak typing and is used throughout JavaScript. Most operators (with the notable exception of the strict equality operators ===
and !==
), and value checking operations (eg. if(value)...
), will coerce values supplied to them, if the types of those values are not immediately compatible with the operation.
The precise mechanism used to coerce a value depends on the expression being evaluated. In the question, the addition operator is being used.
The addition operator will first ensure both operands are primitives, which, in this case, involves calling the valueOf
method. The toString
method is not called in this instance because the overridden valueOf
method on object x
returns a primitive value.
Then, because one of the operands in the question is a string, both operands are converted to strings. This process uses the abstract, internal operation ToString
(note: capitalized), and is distinct from the toString
method on the object (or its prototype chain).
Finally, the resulting strings are concatenated.
Details
On the prototype of every constructor function object corresponding to every language type in JavaScript (ie. Number, BigInt, String, Boolean, Symbol, and Object), there are two methods: valueOf
and toString
.
The purpose of valueOf
is to retrieve the primitive value associated with an object (if it has one). If an object does not have an underlying primitive value, then the object is simply returned.
If valueOf
is invoked against a primitive, then the primitive is auto-boxed in the normal way, and the underlying primitive value returned. Note that for strings, the underlying primitive value (ie. the value returned by valueOf
) is the string representation itself.
The following code shows that the valueOf
method returns the underlying primitive value from a wrapper object, and it shows how unmodified object instances that do not correspond to primitives, have no primitive value to return, so they simply return themselves.
console.log(typeof new Boolean(true)) // 'object'console.log(typeof new Boolean(true).valueOf()) // 'boolean'console.log(({}).valueOf()) // {} (no primitive value to return)
The purpose of toString
, on the other hand, is return a string representation of an object.
For example:
console.log({}.toString()) // '[object Object]'console.log(new Number(1).toString()) // '1'
For most operations, JavaScript will silently attempt to convert one or more operand(s) to the required type. This behavior was chosen to make JavaScript easier to use. JavaScript initially did not have exceptions, and this may have also played a role in this design decision. This kind of implicit type conversion is called type coercion, and it is the basis of JavaScript's loose (weak) type system. The complicated rules behind this behavior are intended to move the complexity of typecasting into the language itself, and out of your code.
During the coercive process, there are two modes of conversion that can occur:
- Conversion of an object to a primitive (which might involve a type conversion itself), and
- Direct conversion to a specific type instance, using a constructor function object of one of the primitive types (ie.
Number()
,Boolean()
,String()
etc.)
Conversion To A Primitive
When attempting to convert non-primitive types to primitives to be operated upon, the abstract operation ToPrimitive
is called with an optional "hint" of 'number', or 'string'. If the hint is omitted, the default hint is 'number' (unless the @@toPrimitive
method has been overridden). If the hint is 'string', then toString
is tried first, and valueOf
second if toString
did not return a primitive. Else, vice-versa. The hint depends on the operation requesting the conversion.
The addition operator supplies no hint, so valueOf
is tried first. The subtraction operator supplies a hint of 'number', so valueOf
is tried first. The only situations I can find in the spec in which the hint is 'string' are:
Object#toString
- The abstract operation
ToPropertyKey
, which converts an argument into a value that may be used as a property key
Direct Type Conversion
Each operator has its own rules for completing their operation. The addition operator will first use ToPrimitive
to ensure each operand is a primitive; then, if either operand is a string, it will then deliberately invoke the abstract operation ToString
on each operand, to deliver the string concatenation behavior we expect with strings. If, after the ToPrimitive
step, both operands are not strings, then arithmetic addition is performed.
Unlike addition, the subtraction operator does not have overloaded behavior, and so will invoke toNumeric
on each operand having first converted them to primitives using ToPrimitive
.
So:
1 + 1 // 2 '1'+ 1 // '11' Both already primitives, RHS converted to string, '1'+'1', '11' 1 + [2] // '12' [2].valueOf() returns an object, so `toString` fallback is used, 1 + String([2]), '1'+'2', 12 1 + {} // '1[object Object]' {}.valueOf() is not a primitive, so toString fallback used, String(1) + String({}), '1'+'[object Object]', '1[object Object]' 2 - {} // NaN {}.valueOf() is not a primitive, so toString fallback used => 2 - Number('[object Object]'), NaN+'a' // NaN `ToPrimitive` passed 'number' hint), Number('a'), NaN+'' // 0 `ToPrimitive` passed 'number' hint), Number(''), 0+'-1' // -1 `ToPrimitive` passed 'number' hint), Number('-1'), -1+{} // NaN `ToPrimitive` passed 'number' hint', `valueOf` returns an object, so falls back to `toString`, Number('[Object object]'), NaN 1 +'a' // '1a' Both are primitives, one is a string, String(1) +'a' 1 + {} // '1[object Object]' One primitive, one object, `ToPrimitive` passed no hint, meaning conversion to string will occur, one of the operands is now a string, String(1) + String({}), `1[object Object]`[] + [] // '' Two objects, `ToPrimitive` passed no hint, String([]) + String([]), '' (empty string) 1 - 'a' // NaN Both are primitives, one is a string, `ToPrimitive` passed 'number' hint, 1-Number('a'), 1-NaN, NaN 1 - {} // NaN One primitive, one is an object, `ToPrimitive` passed 'number' hint, `valueOf` returns object, so falls back to `toString`, 1-Number([object Object]), 1-NaN, NaN[] - [] // 0 Two objects, `ToPrimitive` passed 'number' hint => `valueOf` returns array instance, so falls back to `toString`, Number('')-Number(''), 0-0, 0
Note that the Date
intrinsic object is unique, in that it is the only intrinsic to override the default @@toPrimitive
method, in which the default hint is presumed to be 'string' (rather than 'number'). The reason for having this, is to have Date
instances translate to readable strings by default, instead of their numeric value, for the convenience of the programmer. You can override @@toPrimitive
in your own objects using Symbol.toPrimitive
.
The following grid shows the coercion results for the abstract equality operator (==
) (source):
Addendum
Note that JavaScript coercion rules are designed to maintain internal consistency of behavior, and are not designed to meet your intuition (although they usually do).
So there are counterintuitive edge cases. For example, the following looks counterintuitive and inconsistent, but it is an artefact of maintaining consistent internal behavior 👇
!![] // true because Boolean([]) => true, [[Negation]](true) => false => [[Negation]](false) => true[] == false // true because [].valueOf() => [], which is not primitive, fallback to [].toString() => '', Boolean('') => false, false === false => true
See also and here in You Don't Know JS.