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 1Four 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 intotrueorfalse, covered in lesson 3.4.
ToPrimitive, in the order the engine tries it
- Ask for
Symbol.toPrimitivefirst If the object has that method, it wins outright and receives the hint ('number','string'or'default'). - Otherwise, follow the hint Hint
'number'or'default'triesvalueOf()thentoString(). Hint'string'triestoString()thenvalueOf(). - 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]'
| Expression | Result | The 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 + true | 2 | ToNumber(true) is 1, and neither side is a string |
[] + [] | '' | both arrays stringify to '', and '' + '' is '' |
[] + {} | '[object Object]' | '' concatenated with '[object Object]' |
null + 1 | 1 | ToNumber(null) is 0 |
undefined + 1 | NaN | ToNumber(undefined) is NaN, and NaN spreads |
'5' * 2 | 10 | * 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) // 8Unary + 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}` throwsTemplate 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 + 2is3first, then3 + '3'concatenates to'33'. On the right,'1' + 2concatenates to'12'immediately, and the string keeps winning, giving'123'. - Which method does a template literal call on a plain object?
toString, thenvalueOfif needed — A template literal asks for hint'string', which triestoString()first and falls back tovalueOf(). Arithmetic and bare+use hint'default', which triesvalueOf()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''viaArray.prototype.toString, andToPrimitive({})is'[object Object]'viaObject.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 becomes0. - Calling
parseIntwithout 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'tryvalueOffirst; hint'string'triestoStringfirst. +concatenates if either primitive is a string; every other arithmetic operator converts to number.Number('')is0, which makes empty input the most dangerous value to convert.