Reducer actions
A discriminated union connects each action type with exactly the data it requires. Narrowing the type property makes the correct fields available in each reducer branch.
type TaskAction = | { type: "added"; task: Task } | { type: "toggled"; id: string } | { type: "deleted"; id: string }; function tasksReducer( tasks: Task[], action: TaskAction ): Task[] { switch (action.type) { case "added": return [...tasks, action.task]; case "deleted": return tasks.filter( (task) => task.id !== action.id ); } }
Safe context
Create context with undefined as its default, then expose a custom Hook that throws when used outside the provider. Consumers receive the real value without repeatedly checking for absence.
Generic components
A generic component can preserve the item type while rendering different lists. The caller supplies data, a stable key function and a renderer; TypeScript keeps all three connected.
type ListProps<T> = { items: T[]; getKey: (item: T) => React.Key; renderItem: (item: T) => React.ReactNode; }; function List<T>({ items, getKey, renderItem }: ListProps<T>) { return ( <ul> {items.map((item) => ( <li key={getKey(item)}> {renderItem(item)} </li> ))} </ul> ); }
- Add a
renamedaction to the reducer union and implementation. - Create typed task-data and dispatch contexts.
- Make custom Hooks throw outside their providers.
- Render the task array using the generic
Listcomponent.
Extension challenge: make the reducer's default branch perform an exhaustive never check so an unhandled action becomes a compiler error.
