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.

Or clone locally

git clone -b challenge/2026-06-07 https://github.com/niltonheck/typedrop.git