Module 2 · Types, Operators and Coercion: Why '5' + 2 Is Not 7
Full Programs: Converting on Purpose
In this lesson
- Write six complete programs that turn text into numbers, numbers into text and any value into true or false, each conversion chosen on purpose.
- Explain why
isNaN("abc")istruewhileNumber.isNaN("abc")isfalse, and whyparseInt("12px", 10)is 12 whileNumber("12px")isNaN. - Format a number for a person with
toFixedandIntl.NumberFormat, and know that both give back a string.
Think of the calculator app on a phone. Under its buttons sits a small core that turns the typed text into numbers, does the sum and turns the answer back into text. Every line of that core is a conversion somebody chose. Lessons 01 and 02 showed the engine converting for you, often by surprise. This lesson converts on purpose, in six programs that grow from that calculator core to one a working programmer would ship.
Seven converters on one table
Lesson 01 met three converters that take any value: Number(), String() and Boolean(). Real programs need four more. A radix is the base of a number system: 10 for everyday numbers, 2 for binary, 16 for hexadecimal.
| Converter | Takes | Gives back | Example | Result |
|---|---|---|---|---|
Number(x) | any value | a number; NaN unless the whole text is a number | Number("12px") | NaN |
String(x) | any value | a string | String(255) | "255" |
Boolean(x) | any value | false for the falsy values, true for everything else | Boolean("0") | true |
parseInt(text, radix) | text (anything else becomes text first) | the whole number at the front; NaN if the front has no digit | parseInt("12px", 10) | 12 |
parseFloat(text) | text | the decimal number at the front, always base 10 | parseFloat("3.5kg") | 3.5 |
n.toFixed(digits) | a number | a string with exactly that many decimals | (1.005).toFixed(2) | "1.00" |
n.toString(radix) | a number | a string written in that base | (255).toString(16) | "ff" |
Read the third column again. toFixed and toString give back a string, not a number, and two of the three Common mistakes below come from forgetting it. So a converter's name tells you what it reads, and the table tells you what type comes out.
A whole string, or the front of one
Number() and parseInt() both turn text into a number, yet they disagree about "12px". The picture shows why.
So Number() fits a value that must be a number from end to end, like a form field. parseInt() and parseFloat() fit text that starts with a number and then says something else, like "12px" or "3.5kg".
Four converter forms
parseInt(text, 10)
parseFloat(text)
value.toFixed(digits)
new Intl.NumberFormat(locale).format(value)
textis the string to read. Anything else is turned into a string first.10is the radix, the base to read in. This track always writes it.digitsis how many digits come after the point, from 0 to 100. The result is a string.localenames a language and a region, such as"en-US"or"bn-BD".newbuilds a formatter, and Module 9 explainsnew.
New here: converting each token with Number() yourself. The starter's nextInt() is exactly Number(next()); here you see both halves. The program reads two numbers and an operator, all three as text.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
const left = next();
const op = next();
const right = next();
const a = Number(left);
const b = Number(right);
const result = op === "+" ? a + b : a * b;
out.push("text joined: " + (left + right));
out.push("converted: " + a + " " + op + " " + b + " = " + result);
console.log(out.join("\n"));
text joined: 75
converted: 7 + 5 = 12
That output is for the input 7 + 5. The first line is Bob's old bug, kept on purpose: two strings joined. The second converts first and adds numbers. The ternary handles + and *; Exercise 2 adds - and /.
Two isNaN functions, and the one that lies
A validator needs one question answered: did this text convert to a real number? NaN is the signal, and JavaScript has two functions that look for it. They answer different questions.
console.log(isNaN("abc"), Number.isNaN("abc"));
console.log(isNaN(""), Number.isNaN(""));
console.log(isNaN(undefined), Number.isNaN(undefined));
console.log(isNaN(NaN), Number.isNaN(NaN));
console.log(Number.isNaN(Number("abc")));
true false
false false
true false
true true
true
The global isNaN(x) runs Number(x) first, then checks. So isNaN("abc") says true about a value that is a string, not NaN. That is the lie: it answers "would this become NaN?" under a name that promises "is this NaN?". Number.isNaN(x) converts nothing, and it is true for the NaN value alone.
Line 2 fools both. Number("") is 0, so an empty field converts cleanly. A validator therefore converts with Number() on purpose, tests with Number.isNaN(), and checks for "" itself when empty input can arrive.
New here: Number.isNaN() as the test, on a value you converted yourself.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
const n = nextInt();
for (let i = 0; i < n; i++) {
const text = next();
const value = Number(text);
out.push(text + ": " + (Number.isNaN(value) ? "not a number" : "the number " + value));
}
console.log(out.join("\n"));
12: the number 12
-4.5: the number -4.5
abc: not a number
1e3: the number 1000
0x1f: the number 31
12px: not a number
Infinity: the number Infinity
That output is for the input 7, then 12 -4.5 abc 1e3 0x1f 12px Infinity. 1e3 is 1 times 10 to the power 3, and 0x1f is hexadecimal for 31; Number() reads both. The last line is a gap: Infinity passes as a number. Exercise 1 closes it.
parseInt's radix, then and now
Before ES5, the 2009 edition of the standard, an engine was allowed to read a leading 0 as octal, base 8. Some old browsers did, so parseInt("08") gave 0, because 8 is not an octal digit. ES5 removed that rule, and Node 22 gives 8. The trap still alive today is 0x. Without a radix, parseInt reads "0x1f" as hexadecimal; with radix 10 it reads "0" and stops at the x.
New here: parseInt with and without its radix, and parseFloat, side by side with Number().
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
const n = nextInt();
for (let i = 0; i < n; i++) {
const text = next();
out.push(text + ": Number " + Number(text) + ", parseInt " + parseInt(text) + ", base 10 " + parseInt(text, 10) + ", parseFloat " + parseFloat(text));
}
console.log(out.join("\n"));
12px: Number NaN, parseInt 12, base 10 12, parseFloat 12
3.5kg: Number NaN, parseInt 3, base 10 3, parseFloat 3.5
08: Number 8, parseInt 8, base 10 8, parseFloat 8
0x1f: Number 31, parseInt 31, base 10 0, parseFloat 0
1e3: Number 1000, parseInt 1, base 10 1, parseFloat 1000
abc: Number NaN, parseInt NaN, base 10 NaN, parseFloat NaN
That output is for the input 6, then 12px 3.5kg 08 0x1f 1e3 abc. Read it row by row. parseInt stops at the point in 3.5kg and at the e in 1e3, because neither is a digit. parseFloat reads both, but it never reads hexadecimal. A prefix reader stops early without any warning.
toFixed hands back text
toFixed(2) rounds a number to two decimals for a person to read. It gives back a string, and it rounds the value the computer stored, which is not always the value you typed. Module 1 printed 0.1 + 0.2 as 0.30000000000000004, because a number is stored as a binary fraction that is close, not exact. 1.005 is stored as 1.00499999999999989..., so it rounds down.
New here: toFixed, its string, and its rounding of the stored value.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
const bill = Number(next());
const percent = Number(next());
const tip = bill * percent / 100;
const tipText = tip.toFixed(2);
out.push("tip: " + tipText);
out.push("total: " + (bill + tip).toFixed(2));
out.push("typeof tipText: " + typeof tipText);
out.push("(1.005).toFixed(2): " + (1.005).toFixed(2));
console.log(out.join("\n"));
tip: 6.00
total: 46.00
typeof tipText: string
(1.005).toFixed(2): 1.00
That output is for the input 40 15, a bill of 40 and a 15 percent tip. The total adds two numbers, bill + tip, and only then calls toFixed. Arithmetic first, text last: that order is the rule, and lesson 04 shows a bill where breaking it costs a cent.
New here: filter(Boolean), which the starter has used since Module 1. Lesson 01 named the falsy values: false, 0, "", null, undefined and NaN, plus -0 and 0n as two more forms of zero. This program prints every one, then five values that only look empty. check is a one-line arrow function, the same shape as the starter's next.
const out = [];
const check = (label, value) => out.push(label + ": " + (Boolean(value) ? "truthy" : "falsy"));
check("false", false);
check("0", 0);
check("-0", -0);
check("0n", 0n);
check("empty string", "");
check("null", null);
check("undefined", undefined);
check("NaN", NaN);
check("the text 0", "0");
check("the text false", "false");
check("one space", " ");
check("empty array", []);
check("empty object", {});
const mixed = [0, "0", "", " ", null, "12", NaN, []];
const kept = mixed.filter(Boolean);
out.push("kept " + kept.length + " of " + mixed.length);
console.log(out.join("\n"));
console.log(kept);
false: falsy
0: falsy
-0: falsy
0n: falsy
empty string: falsy
null: falsy
undefined: falsy
NaN: falsy
the text 0: truthy
the text false: truthy
one space: truthy
empty array: truthy
empty object: truthy
kept 4 of 8
[ '0', ' ', '12', [] ]
A string is falsy only when it is empty, so "0" and "false" are truthy. An array or an object is truthy even when empty. The last line is Node's display of an array, with strings in single quotes. So the starter's filter(Boolean) drops the empty strings at the edges of the input and keeps a token "0".
Intl.NumberFormat, your first built-in API
An API, an application programming interface, is a set of ready-made tools a system offers to your code. Intl is a built-in API of both browsers and Node, defined in its own standard, ECMA-402. Intl.NumberFormat knows each region's rules for digits, grouping and currency. The Playground's Node 22 carries the full data for every region, so "bn-BD" prints Bangla digits.
New here: one number formatted three ways, with Intl.NumberFormat. Zara's delivery app logs trips in metres. Her report shows kilometres in English and in Bangla, the miles, and a fuel cost in US dollars.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
const n = nextInt();
const en = new Intl.NumberFormat("en-US");
const bn = new Intl.NumberFormat("bn-BD");
const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
let totalKm = 0;
for (let i = 0; i < n; i++) {
const metres = Number(next());
const km = metres / 1000;
totalKm += km;
out.push(en.format(km) + " km | " + bn.format(km) + " km | " + en.format(km * 0.621371) + " mi");
}
out.push("total: " + en.format(totalKm) + " km");
out.push("fuel at $0.12 a km: " + usd.format(totalKm * 0.12));
console.log(out.join("\n"));
2.5 km | ২.৫ km | 1.553 mi
42.195 km | ৪২.১৯৫ km | 26.219 mi
1,234,567.891 km | ১২,৩৪,৫৬৭.৮৯১ km | 767,124.685 mi
total: 1,234,612.586 km
fuel at $0.12 a km: $148,153.51
That output is for the input 3, then 2500 42195 1234567891; the last trip is a truck's lifetime odometer. The same number reads 1,234,567.891 in en-US and ১২,৩৪,৫৬৭.৮৯১ in bn-BD, grouped in lakhs. The currency formatter adds the dollar sign and rounds to cents. Every format call returns a string, so formatting is the last step.
Where this is used
- React Hook Form. A form field's value is always a string. The library's
registeroptionvalueAsNumber: truehands your code a number, and its documentation saysNaNcomes back when the conversion fails. - Express. The Node web framework puts a URL's query string into
req.query, so?page=2arrives as the string"2". Its documentation warns that these values are user input, to be checked before use. - Next.js Commerce. Vercel's open-source store template receives each price as a string. Its
Pricecomponent converts it withparseFloatand formats it withIntl.NumberFormatin thecurrencystyle.
Common mistakes
1. Adding toFixed's text to a number.
const tip = 6;
const bill = 40;
const total = tip.toFixed(2) + bill;
console.log(total);
6.0040
No error, just a wrong answer. tip.toFixed(2) is the string "6.00", so + joins text. Write (tip + bill).toFixed(2): numbers first, text once at the end. You will make this mistake because "6.00" looks exactly like a number.
2. Calling toFixed on a token.
const price = "3.14159";
console.log(price.toFixed(2));
TypeError: price.toFixed is not a function
The Playground's verdict is Runtime error. price is a string, as every token from next() is, and strings have no toFixed. Write Number(price).toFixed(2). You will forget because the input looked like a number when you typed it.
3. parseInt on a price with a thousands comma.
const price = "1,299";
console.log(parseInt(price, 10));
1
No error, and the price is now 1. parseInt stops at the comma and returns the front. Number("1,299") is NaN, which at least fails loudly. Remove the commas first (Module 3 shows how), or ask for input without them. A loud NaN beats a quiet wrong 1.
Zara feeds Example 2 the token 1e400, which is too big for a number, and it says "the number Infinity". She wants a validator that treats endless values apart.
Input. A line with n, then n tokens with no spaces inside.
Output. One line per token: yes and String(Number(t)) when the value is a finite number, infinite when it is Infinity or -Infinity, and no when it is NaN.
Constraints. n is at least 1. A token is any text without spaces.
Sample. Input 5, then 12 abc Infinity -0 1e400, gives yes 12, no, infinite, yes 0 and infinite on five lines.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Not graded on its own. clean-numbers grades the base validator. Number.isFinite(x) is true only for a number that is neither NaN nor endless, and, like Number.isNaN, it converts nothing.
Grow Example 1 into Alice's full calculator. It reads many sums at once and answers each one the way JavaScript would.
Input. A line with n, then n lines a op b: three tokens, where op is one of +, -, * and /.
Output. For each line, the result of Number(a) op Number(b), printed as JavaScript prints it. Infinity, -Infinity and NaN are answers too.
Constraints. 1 <= n <= 10000. Each of a and b is a decimal number of 1 to 20 characters, maybe negative, maybe with a decimal point.
Sample. Input 5, then 7 + 5, 7 - 10, 0.1 * 3, 1 / 0 and 0 / 0 on five lines, gives 12, -3, 0.30000000000000004, Infinity and NaN on five lines.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Graded as calculator. The hidden tests divide by zero, and they catch a program that adds two tokens without converting them.
Kenji says the integer part of a number is the same however you take it. Test him with three methods: Math.trunc(Number(t)), which cuts off the decimals; parseInt(t, 10); and Math.floor(Number(t)), which rounds down.
Input. A line with n, then n tokens, each a number written as text.
Output. One line per token: the token, a colon and the three results in that order. Then same if all three are equal with ===, or differ.
Constraints. n is at least 1. Every token converts with Number() to a finite number.
Sample. Input 3, then 7.9 -7.9 1e21, gives 7.9: 7 7 7 same, -7.9: -7 -7 -8 differ and 1e21: 1e+21 1 1e+21 differ on three lines.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Not graded on its own. Find a token of your own, beyond the sample, that makes them differ. Which of the three would you trust for a price, and which for a page number?
Run in CompilerCommon doubts
Is
+textthe same asNumber(text)?Yes. The unary plus runs the same conversion, so
+"42"is 42. This track writesNumber(text)because it says what it does. A reader can miss a lone+.Why is
Number("")0 and notNaN?The standard says empty text, or text of only spaces, converts to 0. It is an old rule that cannot change now without breaking websites. So a validator checks for
""before it trusts the conversion.Can I use
toFixedto round money?Only to show it. It returns text, and it rounds the stored binary value, so
(1.005).toFixed(2)is"1.00". Lesson 04 counts money in whole cents instead.Why does
bn-BDgroup the digits as 12,34,567?That is the South Asian lakh and crore grouping: three digits first, then pairs.
Intltakes each region's rule from a shared database of locale data called CLDR, the Unicode Common Locale Data Repository."en-IN"groups the same way with Latin digits.
Key takeaways
Number()wants the whole text to be a number;parseIntandparseFloatread the front and stop quietly.- Always give
parseIntits radix:parseInt(text, 10). The old"08"trap is gone, but"0x1f"is still read as hexadecimal. - Test with
Number.isNaNandNumber.isFinite, which convert nothing; the globalisNaNconverts first and answers a different question. toFixed,toStringandIntl.NumberFormatall give back a string: do the arithmetic first and format once, at the end.- The falsy values are
false,0,"",null,undefinedandNaN, plus-0and0n;"0"," ",[]and{}are truthy. - Go deeper: Interview Pack, output puzzles, the questions and the bug gallery (Pro).
Next, Kenji leaves a review comment that says "never use ==", on the one line where == was right, and lesson 04 settles it.
End of lesson 3
Mark it done, and your progress moves with you.
Next: When to Use == and When Not, and the Other Judgement Calls