useReducer with a discriminated union and an exhaustive switch
Each button dispatches one action, and the log shows the list after each render. Add | { type: "renamed"; id: number; text: string } to Action without a case: the preview still runs (types are stripped), but tsc reports Type '{ type: "renamed"; id: number; text: string; }' is not assignable to type 'never'. at the unhandled line.
Let useState infer simple types, but give it a type argument when the start value is null or an empty array; type DOM refs as useRef<HTMLInputElement>(null); and type reducer actions as a discriminated union so a switch with a never check proves every action is handled.
Change the code and press Run (Ctrl+Enter). Predict what will happen first, then break it on purpose and read what changes. Your edits are saved and match the lesson page.
Ready for more? This phase has 5 hands-on exercises with automatic checks.
Practice questions
Write the code in the editor, run it, then open the model answer to compare.
Write a Counter reducer with actions { type: "increment" }, { type: "decrement" } and { type: "set"; value: number }, with an exhaustive never check, and buttons for each.
Write a component with useState<"idle" | "loading" | "done">("idle") and a button that goes to loading, then to done after a 300 ms fake request. Show the status and log each change.
Explain it without notes
When does useState need a type argument, and what goes wrong without one?
How do you type a ref to an input, and why is current possibly null?
What is a discriminated union, and why is it a good fit for reducer actions?
How does the never trick make a switch exhaustive?
Preview
Console
Nothing logged yet.Expected console output
(empty)
[ ] milk
[ ] milk, [ ] bread
[x] milk, [ ] bread
[ ] bread