0 day streak
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.