A small typed app
Everything at once, on a page: a shape for each item, a typed list, functions with signatures, and a DOM query the compiler can vouch for.
The app, and what to type first
The last chapter is a to-do list — the same shape of program the JavaScript course ended with, written the other way around. Instead of writing behaviour and hoping, the types come first: what an item is, what the list holds, what each function takes and gives back.
That order is the point of the course. A small program that was described before it was written has no untyped corners: every function says what it needs, and the page can be read from the types down. The types are the plan; the functions are the work carried out under it.
The shape of the whole program
interface Todo {
text: string;
done: boolean;
}
const todos: Todo[] = [];
function add(text: string): void {
todos.push({ text, done: false });
render();
}
function render(): void {
const list = document.querySelector<HTMLUListElement>("#list");
if (!list) return;
list.innerHTML = todos.map((todo) => "<li>" + todo.text + "</li>").join("");
}What each part is responsible for
The interface is the vocabulary — everything else is written in it. The list is typed with it, so todos.push(42) is impossible. add takes text and answers with nothing, which its return type states. render narrows the query before touching the page. It returns early rather than throwing, because a missing list is a reason to draw nothing.
Nothing here is clever, and that is the aim. A typed program reads like a description of itself: the shapes up top, the functions under them, and the page updated at the one place that knows how.
State changes, then render
A typed list does not render itself. Every function that changes todos has to call render afterwards, or the page keeps showing the old array. The type system checks the data; the order of those two steps is still your job.
Keeping one render function is what makes that manageable. Add, toggle and remove all end in the same call, and the page has one place that knows how to draw the current state.
Identity, not position
A to-do has no id in this small app, so a toggle by index is the shortest thing that works. The moment items can be removed or reordered, the index stops identifying the item: the third row is a different task after a deletion.
The typed fix is an id: number on the interface and a find by that id. It is more code, and it is the difference between a demo and a list that survives editing. For a list that only ever grows, the index is an honest shortcut; write it down as one, and move to ids before the first delete ships. The compiler cannot see the difference, which is why the habit has to be deliberate.
Where the types stop and the program starts
It is worth noticing what the types did not decide. They did not decide that add should render, that the list should be drawn with innerHTML, or that a todo has no id. Those are program decisions, and the compiler only checks that the code agrees with them after they are made.
That is the honest division of labour. A type can prove that every todo has text and done; it cannot prove that the user sees the new item, that the markup is safe, or that the index still points at the right row. Those are the parts you test by running the page.
What goes wrong
The first failure is a list that updates in memory and not on screen. todos.push succeeds, the type is happy, and the old HTML stays — the compiler has nothing to say, because both statements are valid. Look for a mutation that is not followed by render.
The second is harder to see: an effect that must happen is not in the type. add promises void, and a void function that forgets to render keeps that promise perfectly. Return types describe values, not effects, so the order of a mutation and a render is a convention the code has to keep. One render function, called from every mutation, is the cheapest way to keep it.
Three ways the last program goes wrong. Mutating the list and forgetting to render leaves the page stale with no error. Building HTML by joining reader text into innerHTML is an injection hole. And using the array index as an identity works until the first removal — after that, toggling one row changes another.
Try these next
- Add a
toggle(index: number)function and a Done button per row. - Give each todo an
idand switch the toggle from an index to that id. - Add a
remaining()function and show the count above the list.
Type the to-do app
Declare a Todo interface with its text and done fields, type the list with it, and give `add` the signature `add(text: string)` that builds a new item.
4 left to do
- An interface describes each item
- The list is typed with that shape
- Adding says it takes text
- Each item carries its done state
The interface goes above the list; the list is typed Todo[]; and the new item is an object of that shape: { text, done: false }.
A typed scoreboard
Describe a player, type the list, and write `leader` so it returns the highest scorer — or `undefined` for an empty list.
3 left to do
- An interface describes a player
- The list is typed with that shape
- An empty list is part of the return type
A reduce can carry the best player so far; type the accumulator as Player | undefined so the empty list is handled rather than crashing.
When a program feels tangled, write the types first and the functions afterwards. If the shapes are right, the work left is mechanical — that is the trade this whole course has been making.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Why does this course write the shapes before the functions?
