TypeDrop

2026-02-28 Challenge

2026-02-28 Medium

Typed Event Aggregator with Windowed Rollups

You're building the analytics backbone of a real-time dashboard for a SaaS platform. Raw telemetry events (page views, clicks, errors, purchases) stream in continuously, and the dashboard needs per-event-type rollups aggregated over fixed time windows — all with zero `any` and full type safety on every event shape and its aggregated form.

Goals

  • Define `TelemetryEvent` as a discriminated union and derive `EventKind` and `ExtractEvent<K>` from it using utility/conditional types — no hand-written string literals.
  • Build `RollupMap` as a mapped type over `EventKind` that associates each event kind with its rollup shape.
  • Implement the generic `Reducer<K>` interface and all four concrete reducers (`pageViewReducer`, `clickReducer`, `errorReducer`, `purchaseReducer`) with correct types.
  • Implement `windowKey` and `aggregateEvents` so that events are bucketed by time window and folded through the correct reducer, returning a fully-typed `Map<string, RollupMap[K]>`.
challenge.ts

// Core event union & kind extraction
type TelemetryEvent = PageViewEvent | ClickEvent | ErrorEvent | PurchaseEvent;
type EventKind = TelemetryEvent["kind"]; // "pageView" | "click" | "error" | "purchase"

// Map each kind to its rollup shape
type RollupMap = { [K in EventKind]: /* matching rollup type */ never };

// Generic reducer interface
interface Reducer<K extends EventKind> {
  reduce(acc: RollupMap[K] | undefined, event: ExtractEvent<K>): RollupMap[K];
}

// Main aggregation function — groups events into time windows and folds them
function aggregateEvents<K extends EventKind>(
  events: TelemetryEvent[],
  kind: K,
  reducer: Reducer<K>,
  windowMs?: number        // default 60_000 ms
): Map<string, RollupMap[K]>
Hints (click to reveal)

Hints

  • For `ExtractEvent<K>`, `Extract<TelemetryEvent, { kind: K }>` is the most concise approach — it leverages distributive conditional types built into `Extract`.
  • Inside `aggregateEvents`, after filtering with `event.kind === kind`, TypeScript may still need a type assertion-free cast — try a type predicate helper `(e): e is ExtractEvent<K> => e.kind === kind` to keep things clean.
  • For `RollupMap`, write `[K in EventKind]: ...` and use a conditional type (or explicit mapping) to select the right rollup type for each key — this is the one place a conditional type inside a mapped type really shines.

Or clone locally

git clone -b challenge/2026-02-28 https://github.com/niltonheck/typedrop.git