TypeDrop
2026-06-07 Challenge
2026-06-07
Medium
Typed Event Log Aggregator with Discriminated Unions
You're building the analytics layer for a SaaS platform's audit dashboard. Raw event log entries stream in as `unknown` from a Kafka consumer; your engine must validate them, narrow each to its correct discriminated-union variant, and produce a strongly-typed per-user activity summary — with zero `any`.
Goals
- Define three discriminated-union event interfaces and a `Result<T, E>` monad with no `any` or type assertions.
- Implement `parseEvent` that narrows `unknown` input to the correct `AuditEvent` variant or returns a structured `ValidationError`.
- Implement `aggregateEvents` that computes all `UserSummary` fields — including de-duplicated paths and nullable average latency — in a single pass.
- Wire both functions together in `processEventLog`, separating valid events from failures and returning a typed summary map.
challenge.ts
// Core discriminated-union types + main pipeline signature
interface BaseEvent {
eventId: string;
userId: string;
occurredAt: string;
}
// Three concrete variants (you define the bodies):
interface PageViewEvent extends BaseEvent { kind: "page_view"; payload: { path: string; durationMs: number } }
interface ApiCallEvent extends BaseEvent { kind: "api_call"; payload: { endpoint: string; statusCode: number; latencyMs: number } }
interface ErrorEvent extends BaseEvent { kind: "error"; payload: { code: string; message: string; fatal: boolean } }
type AuditEvent = PageViewEvent | ApiCallEvent | ErrorEvent;
// Generic Result monad (you define this):
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
interface ValidationError { rawInput: unknown; reason: string; }
// Main pipeline — wire parseEvent → aggregateEvents:
function processEventLog(rawEvents: unknown[]): {
summary: Record<string, UserSummary>;
failures: ValidationError[];
}
Hints (click to reveal)
Hints
- Use a `switch` on `kind` inside `parseEvent` — TypeScript will narrow the union for you in each branch, letting you return the correctly typed variant without casting.
- For the single-pass requirement in `aggregateEvents`, maintain a mutable accumulator object per userId (inside a `Record`) and update all counters inside one `for…of` loop.
- `avgApiLatencyMs` needs both a running sum and a count — store both in the accumulator, then compute the mean after the loop finishes.
Useful resources
Or clone locally
git clone -b challenge/2026-06-07 https://github.com/niltonheck/typedrop.git