JavaScript converts, whether you asked or not
Most operators will coerce their operands to make an operation possible. That is convenient about half the time and the source of the language's worst reputation the rest. The fix is not to memorise the rules — it is to convert explicitly and compare with ===.
"5" + 3 // "53" — + prefers strings
"5" - 3 // 2 — every other operator prefers numbers
[] + {} // "[object Object]"
[] == false // true
null >= 0 // true, but null > 0 and null == 0 are both false
Converting on purpose
Number("12.5") // 12.5
Number("12px") // NaN — strict
Number("") // 0 ← watch this one
Number(null) // 0
Number(undefined) // NaN
Number(true) // 1
parseInt("12px", 10) // 12 — stops at the first non-digit
parseFloat("1.5rem") // 1.5
+"12.5" // 12.5 — terse, and easy to miss when reading
parseInt("08") is 8 today, but the two-argument form is unambiguous and costs three characters.
To a string, and to a boolean
String(12.5) // "12.5"
String(null) // "null"
(12.5).toFixed(2) // "12.50" — a STRING
`${12.5}` // "12.5"
Boolean("") // false
Boolean("0") // true ← a non-empty string is truthy
!!value // the same thing, shorter
Truthiness: the eight falsy values
false
0
-0
0n
""
null
undefined
NaN
// truthy, and people get caught by all of these:
[] {} "0" "false" " " function(){}
if (list) is true even for an empty array. Test list.length when you mean "has items".
=== always
== is transitive-breaking: 0 == "" and 0 == "0" are both true while "" == "0" is false. There is no mental model that makes that safe.
The one acceptable use of == is x == null, which is true for exactly null and undefined — a genuinely useful test.
| Comparison | Result | Why |
|---|---|---|
0 == "" | true | Both become 0. |
0 == "0" | true | Both become 0. |
"" == "0" | false | Both are strings, so no conversion. |
null == undefined | true | A special case in the spec. |
null === undefined | false | Different types. |
NaN === NaN | false | NaN is not equal to anything, including itself. |
[] == false | true | [] → "" → 0, false → 0. |
NaN
NaN spreads: once it appears, every arithmetic result downstream is NaN. When a total comes out NaN, work backwards to the first input that was not a number.
0 / 0 // NaN
Number("abc") // NaN
Math.sqrt(-1) // NaN
NaN === NaN // false
Number.isNaN(NaN) // true — use this
isNaN("abc") // true — the GLOBAL one coerces first, so it lies
Number.isNaN("abc") // false — "abc" is not the NaN value
Object.is(NaN, NaN) // true
Checking a type
typeof x === "string"
typeof x === "number" && Number.isFinite(x)
typeof x === "function"
Array.isArray(x) // typeof [] is "object"
x === null // typeof null is "object"
x instanceof Date
Object.prototype.toString.call(x) // "[object Date]" — the last resort
Reading the numbers you show people
Thousands separators, decimal marks and currency positions differ by locale. Intl knows all of them; a template literal does not.
new Intl.NumberFormat("az").format(1234.5); // "1.234,5"
new Intl.NumberFormat("en-GB", {
style: "currency", currency: "GBP",
}).format(1234.5); // "£1,234.50"