Type Coercion

Mental model: When an operator needs a type it does not have, JavaScript runs a fixed conversion recipe. Learn the recipe and the surprises disappear.

Level: beginner · about 13 minutes

console.log(1 + '1');     // '11'  the number became a string
console.log(1 - '1');     // 0     the string became a number
console.log('5' * '2');   // 10    both became numbers
console.log(true + true); // 2     each true became 1

Four lines that look inconsistent. They are not.

Coercion is the implicit conversion an operator performs when its operand is the wrong type. It is not random and it is not a joke. The spec defines a handful of abstract operations, and every result below falls out of them: ToPrimitive, ToNumber, ToString and ToBoolean.

  • ToPrimitive(value, hint) turns an object into a primitive.
  • ToNumber(value) turns a primitive into a number.
  • ToString(value) turns a primitive into a string.
  • ToBoolean(value) turns anything into true or false, covered in lesson 3.4.

ToPrimitive, in the order the engine tries it

  1. Ask for Symbol.toPrimitive first If the object has that method, it wins outright and receives the hint ('number', 'string' or 'default').
  2. Otherwise, follow the hint Hint 'number' or 'default' tries valueOf() then toString(). Hint 'string' tries toString() then valueOf().
  3. If it is still an object, throw When neither method returns a primitive you get a TypeError. This is the only failure mode in the whole algorithm.

+ is two operators wearing one symbol

Binary + first converts both operands to primitives. Then, if either primitive is a string, it concatenates. Otherwise it adds numerically. Every other arithmetic operator skips the string branch entirely, which is why -, * and / never surprise you the same way.

console.log([].toString());       // ''       an empty array joins to nothing
console.log([1, 2].toString());  // '1,2'
console.log({}.toString());      // '[object Object]'
console.log([] + []);            // ''       '' + ''
console.log([] + {});            // '[object Object]'
ExpressionResultThe mechanical reason
1 + '1''11'one primitive is a string, so + concatenates
1 - '1'0- has no string mode, so ToNumber('1') gives 1
true + true2ToNumber(true) is 1, and neither side is a string
[] + []''both arrays stringify to '', and '' + '' is ''
[] + {}'[object Object]''' concatenated with '[object Object]'
null + 11ToNumber(null) is 0
undefined + 1NaNToNumber(undefined) is NaN, and NaN spreads
'5' * 210* converts both sides to numbers
1 + 2 + '3''33'left to right: 3, then 3 + '3'
'1' + 2 + 3'123'left to right: '12', then '12' + 3

Converting to a number on purpose

Unary + and Number(): all or nothing

+'42'      // 42
+' 7 '     // 7
+''        // 0
+'12px'    // NaN
+true      // 1
+[]        // 0

parseInt / parseFloat: read a prefix

parseInt('12px', 10)   // 12
parseInt('px12', 10)   // NaN
parseFloat('3.5rem')   // 3.5
parseInt('', 10)       // NaN
parseInt('08', 10)     // 8

Unary + is the same conversion == and arithmetic use, so it is the honest choice when you want to know whether a whole string is numeric. Always pass the radix to parseInt, and remember Number('') is 0 while parseInt('') is NaN.

const items = [1, 2, 3];
console.log(`items: ${items}`);          // 'items: 1,2,3'
console.log(`obj: ${{ a: 1 }}`);          // 'obj: [object Object]'
console.log(`nothing: ${null} ${undefined}`); // 'nothing: null undefined'
console.log(String(Symbol('id')));       // 'Symbol(id)', but `${sym}` throws

Template literals always take the string path.

console.log([1 + '2', '3' - 1, [] + [], true + '1']);

1 + '2' concatenates because one side is a string. '3' - 1 uses -, which has no string mode, so '3' becomes 3. [] + [] is '' + ''. true + '1' sees a string on the right, so true becomes 'true' and the result is 'true1'.

Try it yourself

Coercion bench

const show = (label, value) =>
  console.log(label.padEnd(16), typeof value, JSON.stringify(value) ?? String(value));

show("1 + '1'",    1 + '1');
show("1 - '1'",    1 - '1');
show('true + true', true + true);
show('[] + []',    [] + []);
show('[] + {}',    [] + {});
show('+null',      +null);
show('+undefined', +undefined);
show("+' 7 '",     +' 7 ');

Add your own expressions. Then add a Symbol.toPrimitive to an object and watch every row involving it change.

Exercises

Make an object that coerces well

Write makeTemperature(celsius) returning an object that behaves sensibly in both worlds: in a numeric position it is the number of degrees (temp + 1, 2 * temp, Number(temp)), and in a string position it reads like '21 C' (String(temp) and a template literal). Do it with valueOf and toString, not Symbol.toPrimitive.

Check yourself

What does this log?
'33' '123' — + groups left to right. On the left, 1 + 2 is 3 first, then 3 + '3' concatenates to '33'. On the right, '1' + 2 concatenates to '12' immediately, and the string keeps winning, giving '123'.
Which method does a template literal call on a plain object?
toString, then valueOf if needed — A template literal asks for hint 'string', which tries toString() first and falls back to valueOf(). Arithmetic and bare + use hint 'default', which tries valueOf() first. Same algorithm, opposite order.
Why is [] + {} the string '[object Object]'?
Because both operands become primitives ('' and '[object Object]') and one is a string, so + concatenates — ToPrimitive([]) is '' via Array.prototype.toString, and ToPrimitive({}) is '[object Object]' via Object.prototype.toString. With a string present, + concatenates. Nothing special happens for objects.

Common mistakes

  • Treating + as one operator. It concatenates the moment either primitive is a string.
  • Using Number(input) on a possibly empty field, where '' quietly becomes 0.
  • Calling parseInt without a radix, or on a value that is not a string.

Takeaways

  • Objects become primitives through ToPrimitive, which is driven by a hint.
  • Hint 'number' and 'default' try valueOf first; hint 'string' tries toString first.
  • + concatenates if either primitive is a string; every other arithmetic operator converts to number.
  • Number('') is 0, which makes empty input the most dangerous value to convert.