Programs, values, and types · Lesson 8 of 77 · concept
Coercion, and the two kinds of equals
About 50 minutes.
By the end of this lesson you can
- Predict whether + will add two values or join them as text.
- Convert text to a number deliberately, and check the conversion worked.
- Explain what == does that === does not, and why === is the default.
- Recognise the one place where a loose == comparison is still the right tool.
Coercion: the language converting things behind your back
Explanation
COERCION is JavaScript quietly converting a value from one type to another because an operator needed it to be something else. It is the source of nearly every joke made at this language's expense, and — used deliberately — also of some of its convenience.
The important word is quietly. Nothing warns you. An operator that wanted a number and was given text simply converts the text and carries on, and if the conversion produced nonsense you find out several steps later, usually as NaN or as a number with too many digits in it.
The cure is not to memorise the conversion tables. It is a habit: convert on purpose, at the edge of your program, and compare strictly everywhere. Do that and the tables stop mattering.
+ is two different operators wearing one hat
Explanation
Every other arithmetic operator has one job. Not +. If either side is a piece of text, + joins them into a longer piece of text; only when both sides are numbers does it add. That single ambiguity causes more beginner confusion than anything else in the language.
So "5" + 2 gives the text 52, while "5" - 2 gives the number 3 — because subtraction has no text meaning at all, so it converts the text to a number and does the arithmetic. The same goes for *, / and %. Only + is ambiguous, and only + is the one you will reach for by accident.
It gets one step worse, because + is worked out left to right. 1 + 2 + "3" gives 33: the numbers are added first, giving 3, and only then does the text turn the result into text. Reverse it and "1" + 2 + 3 gives 123, because once the running result is text everything after it is joined rather than added. Nothing here is random, but nothing here is guessable either.
Converting on purpose
Explanation
Number(value) converts to a number, String(value) converts to text, and both say plainly in the code what is happening. Number("42") gives 42. Number("twelve") gives NaN, which is the honest answer and is why you should check it with Number.isNaN before trusting the result.
Two conversions catch people out. Number("") is 0 — empty text becomes zero, not NaN — so an empty form field can silently become a real quantity. And parseInt("12px", 10) gives 12, because parseInt reads as far as it can and stops, whereas Number("12px") gives NaN because it insists the whole thing be a number. Neither is better; they answer different questions. Use Number when the whole value must be a number, parseInt when you are deliberately pulling a number off the front of something messy. Always pass 10 as the second argument to parseInt, so it can never guess a different base.
== against ===
Explanation
=== asks: same type, same value? If the types differ the answer is false and no conversion happens. == asks a looser question: could these be made equal if I converted one of them? So "1" == 1 is true, and 0 == "" is true, and null == undefined is true.
The trouble is that the conversions are not consistent with each other and cannot be reasoned out from first principles. null == undefined is true, but null == 0 is false even though many other conversions treat null as zero. Two values can each be loosely equal to a third and not to one another. There is a full table in the specification, and its existence is the argument against ever relying on it.
Hence the rule that essentially every JavaScript style guide states: use === and !== always. Not usually. Always — with one exception, below.
The one loose comparison worth keeping
A mental model
value == null is true for exactly two values: null and undefined. Nothing else. Not 0, not empty text, not false.
That makes it a precise, idiomatic way to ask the question you learned to ask in the last lesson: is there nothing here, of either kind? The strict version needs both halves — value === null || value === undefined — and says the same thing at twice the length.
It is worth knowing rather than worth insisting on: many teams ban == outright and write the long version, which is also fine. What matters is that if you do see == in good code, this is almost certainly what it is doing, and that you never write == for anything else.
The bug you will actually hit: numbers that arrive as text
A common mistake
Everything a program receives from outside arrives as text. A form field, a value read from a file, a number in a web address — all text, even when every character in it is a digit.
So the classic bug is arithmetic that silently becomes joining. A quantity of "2" plus 1 more gives "21" rather than 3, and nothing anywhere reports an error; a total is simply wrong on a screen somewhere. Worse, it may work by accident for a while, because every other operator converts properly — the multiplication in the same file is fine, so the value looks like a number until the day somebody adds to it.
The fix is a discipline, not a trick: convert at the boundary. The moment a value enters your program, turn it into the type it is supposed to be, check the conversion succeeded, and let everything inside work with real numbers. Module 7 builds that into a habit called validating at the boundary.
Recap
Recap
Coercion is automatic type conversion. + joins when either side is text and adds only when both are numbers; every other arithmetic operator converts to a number. Convert deliberately with Number and String, and check for NaN. Use === everywhere; the single defensible == is value == null, which catches null and undefined and nothing else.
Worked examples
Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.
The same two values, four different answers
Each line prints the result and then the kind of value it is, because that is the part you cannot see from the digits alone.
1console.log("5" + 2, typeof ("5" + 2));2console.log("5" - 2, typeof ("5" - 2));3console.log(1 + 2 + "3");4console.log("1" + 2 + 3);- line 1
One side is text, so + joins. The printed 52 is a two-character piece of text, which is why the typeof matters.
- line 2
Subtraction has no text meaning, so the text 5 is converted to a number first. The answer is a real number.
- line 3
Left to right: 1 + 2 is 3, then 3 + text 3 joins into 33.
- line 4
Same rule, different starting point: once the running value is text, everything after it is joined.
What it prints
52 string3 number33123
Converting on purpose, and checking it worked
Number, String and parseInt, with the two results that surprise people: empty text becomes zero, and parseInt is happy to stop early.
1console.log(Number("42") + 1);2console.log(Number("") === 0);3console.log(Number.isNaN(Number("twelve")));4console.log(parseInt("12px", 10));5console.log(Number.isNaN(Number("12px")));6console.log(String(42) + 1);- line 1
Converted first, so + adds. 43, a number.
- line 2
Empty text converts to zero, not to NaN. A blank form field can therefore become a real quantity of nothing.
- line 3
Text that is not a number at all becomes NaN, and this is how you detect it.
- line 4
parseInt reads digits from the front and stops at the x. The 10 says read it as base ten.
- line 5
Number refuses the same value outright, because the whole thing is not a number.
- line 6
Conversion in the other direction: now + joins, giving the text 421.
What it prints
43truetrue12true421
Six comparisons that explain the rule
Read each pair together. The == answers are not wrong, they are answers to a question you did not ask.
1console.log(0 == "");2console.log(0 === "");3console.log(null == undefined);4console.log(null === undefined);5console.log(null == 0);6console.log("1" == 1);- line 1
Empty text converts to 0, so == says these match. A blank field would pass a check for the number zero.
- line 2
Different types, so === stops there. This is the answer you almost always want.
- line 3
The useful case: == treats the two nothings as one, which is the value == null idiom.
- line 5
And the inconsistency: null converts to 0 in arithmetic, but == deliberately refuses to compare it with a number. There is no principle here to derive, only a table.
- line 6
Text 1 and number 1 match loosely. This is the comparison behind most == bugs.
What it prints
truefalsetruefalsefalsetrue
Check yourself
Not scored, not stored. Getting one wrong is the useful part.
A quantity arrived from a form as text and five more were added. What is printed?
const total = "10" + 5;console.log(total);console.log(typeof total);
Which of these comparisons is true?
A form field gives you the text "34" and you need to do arithmetic with it. What is the right first move?
Write it yourself
Exercise · core · 5 tests
An age that arrived as text
typedAge holds what a user typed into a form: the text 34, not the number. Declare age, the same value converted properly to a number; nextYear, that number plus one; and wrongNextYear, the result of adding 1 to the ORIGINAL text without converting it — so you can see the bug you are avoiding.
age is the number 34, nextYear is the number 35, and wrongNextYear is the text 341, because + joins when either side is text. All three tests check the type as well as the value, since a right-looking answer of the wrong type is the failure mode this exercise exists to show.
Define age, nextYear and wrongNextYear at the top level of your code — the tests call them by name.
Exercise · stretch · 5 tests
A basket line built from two text fields
A quantity and a price both arrive as text. Declare total, the two values converted to numbers and multiplied; totalLabel, that total shown to exactly two decimal places; and naiveSum, what you would get by joining the two original texts with + instead — the wrong answer, written on purpose.
total is the number 13.5, totalLabel is the text 13.50, and naiveSum is the text 34.50. Note that multiplying the two texts without converting would also have given 13.5, because * converts and only + does not — so the correct-looking result of a wrong habit is part of what this exercise is showing you.
Define total, totalLabel and naiveSum at the top level of your code — the tests call them by name.
Worth remembering
- When does + add and when does it join?
It joins as text if either side is text, and adds only when both sides are numbers. Every other arithmetic operator converts to a number first, so only + is ambiguous.
- What is the rule about == and ===?
Use === and !== everywhere. The single defensible loose comparison is value == null, which is true for null and undefined and nothing else.
- Where should a text value be converted to a number?
At the boundary — the moment it enters the program — with Number(), followed by a Number.isNaN check. Everything inside then works with real numbers.
Check this lesson against the source
We wrote the explanation; we did not invent the facts. This is the page that backs them.
Check there: That == converts the operands to a common type before comparing, and the description of null == undefined being true while null == 0 is false.
Check there: That operands of different types are never strictly equal, with no conversion attempted.
Check there: That + performs string concatenation when either operand is a string, and numeric addition otherwise.
Check there: That parseInt stops at the first character that is not a valid digit, and the purpose of the radix argument.
Prefer to read the source in a structured breakdown? Turn this doc into a breakdown