Chapter 21
Interlude: Build Something Real
No new syntax. An expense tracker that remembers.
Nothing new here. No syntax, no methods, no compiler flags you haven’t already met.
That’s the point. You have twenty chapters of tools and you have used them one or two at a time, on programs written to demonstrate them. This chapter is one program that needs most of them at once, which is a different skill and the one that actually matters.
What you’re building
An expense tracker. You record what you spent, and it tells you where the money went and remembers between runs.
$ printf 'add 3.50 food Coffee\n' | node main.js
Added: Coffee $3.50 (food)
$ printf 'add 2.40 travel Bus fare\n' | node main.js
Added: Bus fare $2.40 (travel)
$ printf 'report\n' | node main.js
Total: $5.90
food: $3.50
travel: $2.40
Three runs, three separate programs starting and stopping, and the third one knows about the first two.
What it uses
Worth seeing the list, because it’s the argument for the last twenty chapters:
| Chapter | Doing what |
|---|---|
| 5 | reading the command from stdin |
| 7, 8 | deciding what the command was, looping over expenses |
| 9 | every piece of work has a name |
| 12 | an Expense shape |
| 14 | .reduce() for the total |
| 17 | the file that makes it remember |
| 18 | a Map for per-category totals |
| 19 | validating the file, because you didn’t write what’s in it |
| 20 | four files, each with one job |
Build it in stages
Do not write all four files and then run it. Get something working, then make it better, five times over. Each stage below runs.
Stage 1: record one expense, in memory
One file. A hardcoded Expense, printed. No input, no file, no split.
type Expense = { title: string; amount: number; category: string };
const e: Expense = { title: "Coffee", amount: 3.5, category: "food" };
console.log(`${e.title} $${e.amount.toFixed(2)} (${e.category})`);
That’s chapter 12 and chapter 4. It runs, and now you have something to improve rather than a blank file.
Stage 2: an array, a total, and categories
Add three or four expenses to an array and report on them. .reduce() for
the total, a Map for per-category sums, both from chapters 14 and 18.
Print the total and each category. Check the category amounts add up to the total by hand, because that’s the kind of number that looks right whether or not it is.
Stage 3: remember it
Now chapter 17. Load at the start, save at the end, existsSync for the
first run.
Run it twice and confirm the second run finds the first one’s data. This is the stage where the program stops being a demonstration.
Stage 4: take a command
Chapter 5’s single read. Split the line, look at the first word:
add 3.50 food Coffee
report
list
add takes an amount, a category, and the rest of the line as a title.
report prints the totals. list prints everything.
The title being “the rest of the line” is the fiddly part, and .slice(3)
on the split words plus .join(" ") is the shortest honest answer.
Chapter 19 applies to the amount: Number(parts[1]) on abc gives you
NaN, and a NaN in a total poisons every number after it. Check it before
you accept it.
Stage 5: split it up
Chapter 20. Four files:
expense.ts the type, the validator, total, byCategory
storage.ts load and save
format.ts money and line formatting
main.ts read the command, decide, print
Move one function at a time and confirm it still runs. Compile with
npx tsc --module commonjs *.ts.
Things that will go wrong
All of these are from earlier chapters, and all of them are easier to recognise than to debug:
- Every total is
NaN. A bad amount got in. Chapter 6’sNaNspreads through every sum that touches it, so one bad entry ruins the report. - The second run loses the first run’s data. You’re writing before loading, or writing inside a loop. One read at the start, one write at the end.
- A category count is one too many. Chapter 8’s phantom empty string
from
.split(), or an empty title counting as a category. Cannot use import statement outside a module. You forgot--module commonjsin stage 5.- Amounts come out as
$3.5not$3.50..toFixed(2)is chapter 6’s, and it returns a string, so it belongs at the printing edge and nowhere near your arithmetic.
What you still can’t do
This is the honest end of Part 1, and the list is what Part 2 is for.
Your tracker reads all input in one go and can’t ask a follow-up question,
because chapter 5’s readFileSync reads to end-of-file and there’s no going
back. Interactive programs need asynchronous code, which is chapters 25
and 26.
You’ve written Array<number> and Map<string, number> and been told the
angle brackets get explained later. They’re generics, chapter 27, and
once you have them you can write a container of your own.
You’ve seen string | number and been told it’s a union. Chapter 28 is
where those become a tool rather than a curiosity, and where narrowing goes
from an if you write to something the type system reasons about.
Chapter 15 showed that private evaporates at runtime and left you with a
# and a promise. Chapter 22 explains what else disappears, and why
“the types are gone” is the single most useful fact about TypeScript.
And your validator in stage 4 is fifteen lines to check three fields. There are better answers, and chapter 29 is about why TypeScript cares about the shape of a thing rather than its name, which is what makes them possible.
Part 2 is not more features. It’s the machinery underneath the twenty chapters you just used.
Project
The expense tracker
Roughly 3 to 4 hours
Everything above, built in the five stages, in your own words and your own file layout.
The finished program must:
- Take
add,listandreportas commands from stdin. - Persist to
expenses.jsonbetween runs. - Total correctly, and break the total down by category.
- Validate the file on load, skipping and reporting bad entries.
- Live in four files, compiled with
--module commonjs.
The tests it must pass, and write these down as you go, because a program you can re-check is worth more than one that happened to work once:
- Add three expenses in three separate runs, then
report. All three appear and the total is right. - The category amounts add up to the total. Check by hand.
- Delete
expenses.jsonand runreport. Empty tracker, no crash. - Put
{"title":"X","amount":"lots","category":"food"}in the file. It’s skipped, reported, and the total ignores it. - Put
{"not":"a list"}in the file. Clear message, empty tracker. add abc food Thing. Rejected before it reaches the file.
Then use it for a week. Genuinely. Add your actual coffees.
You will find something annoying about it within two days, and that annoyance is worth more than this chapter: it’s the first time in this book that you’ll be changing a program because you want it different, rather than because an exercise said to. Fix it. That’s what the rest of programming is.