typescript / expert
Snippet
Exhaustive Reducers over Discriminated Event Streams
`reduce<Counts>` pins the accumulator's type so the callback's return value is checked structurally on every branch instead of being widened to `Event | Counts`. Inside the switch, narrowing on the `kind` discriminant gives each case its specific payload. The `never`-typed default turns adding a new variant into a compile error at the reducer, not a silent miscount at runtime.
snippet.ts
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type Event =| { kind: 'click'; x: number; y: number }| { kind: 'key'; code: string }| { kind: 'scroll'; delta: number };type Counts = { click: number; key: number; scroll: number };function tally(events: readonly Event[]): Counts {return events.reduce<Counts>((acc, e) => {switch (e.kind) {case 'click':return { ...acc, click: acc.click + 1 };case 'key':return { ...acc, key: acc.key + 1 };case 'scroll':return { ...acc, scroll: acc.scroll + 1 };default: {const _exhaustive: never = e;return _exhaustive;}}},{ click: 0, key: 0, scroll: 0 },);}
Breakdown
1
type Event = | { kind: 'click'; ... } | { kind: 'key'; ... } | { kind: 'scroll'; ... };
A discriminated union keyed by `kind` lets each variant carry its own payload shape.
2
events.reduce<Counts>((acc, e) => { ... }, { click: 0, key: 0, scroll: 0 });
The explicit type parameter binds the accumulator type to Counts for every callback return.
3
switch (e.kind) { ... }
Switching on the discriminant narrows `e` to one specific variant inside each case.
4
const _exhaustive: never = e;
Only `never` is assignable to `never`; an unhandled variant breaks the build here.
5
return { ...acc, click: acc.click + 1 };
Spreading instead of mutating keeps the reducer pure and safe for parallel iteration.