TypeDrop

2026-07-10 Challenge

2026-07-10 Medium

Typed Event Aggregator with Time-Window Bucketing

You're building the analytics ingestion layer for a real-time monitoring dashboard. Raw events arrive as `unknown` from a webhook stream; your aggregator must validate them into strongly-typed `MonitoringEvent` records, bucket them into fixed time windows, and produce a fully-typed `WindowSummary` per bucket — with zero `any`.

Goals

  • Implement `parseEvent` to validate `unknown` payloads and narrow them into the correct `MonitoringEvent` discriminated-union branch, returning `null` for any malformed input.
  • Implement `getBucketKey` to compute the ISO-8601 window-start string for a given timestamp and window duration.
  • Implement `aggregateEvents` to validate, bucket, and aggregate events into per-window `WindowSummary` objects with correct category counts, severity counts, metric stats, and deduplicated sources.
  • Implement the generic `groupBy<T, K extends string>` helper so its return type preserves the caller's specific key type K.
challenge.ts
export type EventCategory = "error" | "warning" | "info" | "metric";
export type Severity      = "low" | "medium" | "high" | "critical";

export interface AlertEvent  extends BaseEvent { category: "error" | "warning"; severity: Severity; message: string; }
export interface InfoEvent   extends BaseEvent { category: "info";   description: string; }
export interface MetricEvent extends BaseEvent { category: "metric"; metricName: string; value: number; }

export type MonitoringEvent = AlertEvent | InfoEvent | MetricEvent;
export type BucketKey       = string;
export type CategoryCounts  = Record<EventCategory, number>;
export type SeverityCounts  = Record<Severity, number>;

export interface WindowSummary {
  windowStart:    BucketKey;
  totalEvents:    number;
  categoryCounts: CategoryCounts;
  severityCounts: SeverityCounts;
  metricStats:    Record<string, MetricStats>;
  sources:        string[];
}

export function parseEvent(raw: unknown): MonitoringEvent | null { /* TODO */ }
export function getBucketKey(timestampMs: number, windowMs: number): BucketKey { /* TODO */ }
export function aggregateEvents(rawEvents: unknown[], windowMs: number): AggregationResult { /* TODO */ }
export function groupBy<T, K extends string>(items: T[], keyFn: (item: T) => K): Map<K, T[]> { /* TODO */ }
Hints (click to reveal)

Hints

  • In `parseEvent`, switch on `category` after confirming it's a valid `EventCategory` — TypeScript will narrow the type inside each `case` branch if you structure the checks correctly.
  • For `CategoryCounts` and `SeverityCounts`, initialise every key explicitly (all four literals) so the `Record<…, number>` contract is satisfied from the start.
  • The `groupBy` generic constraint `K extends string` lets TypeScript infer K from the return type of `keyFn` at the call site — you don't need to annotate it manually.

Or clone locally

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