Chapter 6
Arithmetic, and the Number Type's Sharp Edges
One numeric type for everything, and what that costs you.
Chapter 3 gave you number and mentioned, in passing, that TypeScript uses
one type where plenty of languages use two or three. That was a promise of
a bill coming due. This is the chapter where it arrives.
Most of arithmetic is exactly what you’d expect, so let’s get through that part quickly and spend the time where it’s actually interesting.
The operators
console.log(10 - 3);
console.log(4 * 2.5);
console.log(7 / 2);
7
10
3.5
Nothing surprising, and one thing worth pointing at: 7 / 2 is 3.5.
If you’ve read about programming before, you may have been warned that
dividing two whole numbers throws the fraction away and gives you 3. That
is true in a lot of languages, C++ among them, and it’s a classic source of
quietly wrong answers. It is not true here. TypeScript has one numeric
type, so there’s no such thing as “an integer division” for it to fall into.
/ always gives you the real answer.
That’s the upside of one number type, and it’s genuine. Hold onto it,
because the rest of this chapter is the other side.
The remainder
% gives you what’s left over after a division:
console.log(7 % 2);
console.log(5.5 % 2);
1
1.5
Seven divided by two is three with one left over, so 7 % 2 is 1. It
works on fractions too, since again, there’s only one kind of number here.
% is more useful than it looks. “Is this number even” is n % 2 === 0,
and “how many seconds are left after taking out whole minutes” is
n % 60, which is exactly what this chapter’s project needs.
Order of operations
*, / and % happen before + and -, the same rule as school maths.
Parentheses override it:
console.log(2 + 3 * 4);
console.log((2 + 3) * 4);
14
20
When in doubt, use parentheses. They cost nothing and they mean the next person to read the line doesn’t have to remember a table.
Changing a value with what’s already in it
let total = 5;
total += 3;
total *= 2;
console.log(total);
16
total += 3 means “add 3 to whatever total already holds.” There’s a
version of this for each operator, -=, *=, /=, and they all read the
same way. There’s also total++, which adds exactly 1 and is what you’ll
see most often once loops arrive in chapter 8.
The bill, part one: decimals
Here’s the cost chapter 3 promised:
console.log(0.1 + 0.2);
0.30000000000000004
That is not a typo, and your computer isn’t broken. Numbers are stored in
binary, and one tenth in binary is a repeating fraction, the same way one
third in decimal is 0.333... forever. It has to be cut off somewhere, so
0.1 isn’t exactly one tenth, it’s extremely close. Add two extremely
close numbers and the tiny errors show up.
Which means this, which looks like it must be true, isn’t:
console.log(0.1 + 0.2 === 0.3);
false
For displaying money, .toFixed() rounds to a fixed number of decimal
places:
console.log((0.1 + 0.2).toFixed(2));
console.log((99).toFixed(2));
0.30
99.00
That solves the chapter 3 project’s slightly-bare 99 too. One catch worth
knowing: .toFixed() gives you back a string, not a number, so it’s
the last thing you do before printing, not something to keep doing
arithmetic with.
The bill, part two: very large whole numbers
The other cost is at the far end of the scale:
console.log(Number.MAX_SAFE_INTEGER);
console.log(Number.MAX_SAFE_INTEGER + 1);
console.log(Number.MAX_SAFE_INTEGER + 2);
9007199254740991
9007199254740992
9007199254740992
Look at the last two lines. Two different sums produced the same answer.
Past about nine quadrillion, there are more whole numbers than number has
room to tell apart, so some of them share.
You are unlikely to hit this. It’s worth knowing anyway, because it’s the
clearest possible statement of what “one type for everything” means:
number is a compromise that covers the enormous middle of what anyone
needs, and gives up a little at both ends.
Exercise 1 · Watch the edge
Print Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 and
confirm you get true. Two sums that are obviously different, comparing
as equal.
It’s the same underlying reason as 0.1 + 0.2: a fixed amount of room to
store a number in, and not every number fitting. Seeing both ends of it
once makes the middle make sense.
Dividing by zero doesn’t crash
In some languages this is a disaster. Here it’s just an answer:
console.log(1 / 0);
console.log(-1 / 0);
console.log(0 / 0);
Infinity
-Infinity
NaN
Infinity is a real value you can hold in a variable and print. And 0 / 0
gives you NaN, which you already met in chapter 5 when Number("banana")
produced it.
Nothing crashes, nothing is undefined, the program keeps running. That’s
genuinely kinder than the alternative, but it does mean a division by zero
slips through silently and turns up later as an Infinity in your output.
Same lesson as NaN: recognise it on sight.
Getting a whole number on purpose
Since / never truncates, “how many whole hours is this” needs asking for:
console.log(7 / 2);
console.log(Math.floor(7 / 2));
3.5
3
Math.floor() rounds down to a whole number. There’s Math.ceil() to
round up and Math.round() for nearest. Math.floor() is the one that
does the job the project needs.
Check yourself
Project
Seconds into hours, minutes and seconds
Roughly 30 minutes
Take a number of seconds and print it as hours, minutes and seconds.
Start with the number hardcoded in a const.
From 3725, produce:
3725 seconds is 1h 2m 5sThe shape of it:
- Hours are how many whole 3600s fit in the total. Division alone
gives you
1.034..., soMath.floor()is doing real work here. - Minutes are the whole minutes in what’s left over after the
hours come out. That leftover is exactly what
%gives you, so this step uses both operators:Math.floor((total % 3600) / 60). - Seconds are what’s left after whole minutes, which is just
total % 60. - Join it with a template literal.
Rules: one const for the input at the top, and changing only that
number should give a correct answer for any value you put in it. No
copy-pasted arithmetic with different numbers written in by hand.
Test it properly. The interesting inputs are the boundaries, not the middle:
| Input | Should print |
|---|---|
59 | 0h 0m 59s |
60 | 0h 1m 0s |
3600 | 1h 0m 0s |
86399 | 23h 59m 59s |
0 | 0h 0m 0s |
If any of those come out wrong, the usual culprit is a missing
Math.floor() giving you a decimal, or taking % 60 of the wrong
number.
Stretch: print 1h 02m 05s instead, with the minutes and seconds
padded to two digits, the way a clock shows them. You’ll need to build a
string and check its length, which is chapter 4’s territory. Doing it
without if is awkward on purpose, and chapter 7 makes it easy.