TypeDrop

A new TypeScript challenge every day. Sharpen your types.

TypeDrop delivers a fresh TypeScript challenge every day, generated by AI. Pick a challenge, open it in StackBlitz (preferred) or CodeSandbox (or clone it locally), and make the tests pass. No accounts, no setup — just you and the type system.

Learn more on GitHub →

2026-04-05 Medium

Typed Event Stream Aggregator

You're building the real-time analytics engine for a live-streaming platform. Raw events arrive as unknown JSON blobs from multiple sources; your engine must validate them, fan out processing concurrently with a limit, and aggregate per-stream statistics into a fully typed report — with zero `any`.

Goals

  • Declare `Brand<T, B>` and apply it to `EventId`, `StreamId`, and `TimestampMs` — then use those branded types throughout `StreamEvent` and `StreamStats`.
  • Implement `validateEvent` using a `Result<T, E>` discriminated union and user-defined type guards to narrow every `unknown` field without `as` or `any`.
  • Implement `aggregateEvents` to fan out `EventProcessor` calls concurrently up to `concurrencyLimit`, then aggregate per-stream `StreamStats` (counts, totals, firstSeen/lastSeen) into an `AggregationReport`.
  • Implement `lookupStream` to convert a plain `string` to a branded `StreamId` via a type guard and return `Result<StreamStats, string>` — no non-null assertions.
challenge.ts
// Core types & main function signature

type Brand<T, B extends string> = T & { readonly __brand: B };

type EventId     = Brand<string, "EventId">;
type StreamId    = Brand<string, "StreamId">;
type TimestampMs = Brand<number, "TimestampMs">;
type EventKind   = "view" | "like" | "comment" | "share" | "donation";

interface StreamEvent {
  id: EventId;
  streamId: StreamId;
  kind: EventKind;
  value: number;
  timestamp: TimestampMs;
}

type Result<T, E> =
  | { ok: true;  value: T }
  | { ok: false; error: E };

type EventProcessor = (event: StreamEvent) => Promise<void>;

// --- implement this ---
async function aggregateEvents(
  rawEvents: RawEvent[],
  processor: EventProcessor,
  concurrencyLimit?: number,
): Promise<AggregationReport> { /* TODO */ }
Hints (click to reveal)

Hints

  • A type guard of the form `function isEventId(x: string): x is EventId` lets you refine a plain string into a branded type without `as` — apply the same pattern for `StreamId` and `TimestampMs`.
  • For `Result<T, E>`, a discriminated union on a boolean `ok` field (`{ ok: true; value: T } | { ok: false; error: E }`) gives you exhaustive narrowing with a simple `if (result.ok)` check.
  • To respect `concurrencyLimit` without a library, slice the validated events array into chunks of that size and `await Promise.all(chunk.map(processor))` — iterate chunk by chunk sequentially.

Or clone locally

git clone -b challenge/2026-04-05 https://github.com/niltonheck/typedrop.git