TypeDrop

2026-06-26 Challenge

2026-06-26 Hard

Typed Event Sourcing Engine with Snapshot & Projection

You're building the core event-sourcing engine for a financial ledger service. Raw domain events arrive as `unknown` from a Kafka consumer; your engine must validate and narrow them into a discriminated-union event log, rebuild aggregate state via typed reducers, apply snapshotting for performance, and fan-out to multiple read-model projections — all with zero `any`.

Goals

  • Implement `validateEvent` to narrow `unknown` raw Kafka payloads into a typed `DomainEvent` discriminated union, returning a `Result<DomainEvent, ValidationError>`.
  • Implement `applyEvent` and `rebuildFromEvents` to reconstruct `AccountState` from an ordered event log, supporting optional `Snapshot<AccountState>` fast-forwarding.
  • Define the `Projection<R>` interface and implement both `ledgerProjection` and `summaryProjection` as pure fold functions over the event stream.
  • Implement `runProjections` using a mapped type over a const-inferred tuple so the return type mirrors the input projections tuple element-for-element, then wire everything together in `ingestEvents`.
challenge.ts
// Key types & main pipeline function

type DomainEvent =
  | AccountOpenedEvent
  | MoneyDepositedEvent
  | MoneyWithdrawnEvent
  | AccountClosedEvent;

type Result<T, E extends string> = Ok<T> | Err<E>;

interface Projection<R> {
  name: string;
  init: () => R;
  apply: (readModel: R, event: DomainEvent) => R;
}

// Replays events through N projections — return type mirrors the input tuple:
function runProjections<
  const Ps extends readonly [Projection<unknown>, ...Projection<unknown>[]]
>(
  events: readonly DomainEvent[],
  projections: Ps
): { [K in keyof Ps]: Ps[K] extends Projection<infer R> ? R : never };

// Full ingestion pipeline: validate → rebuild aggregate → project
function ingestEvents(rawEvents: readonly unknown[]): PipelineResult;
Hints (click to reveal)

Hints

  • For `runProjections`, the return type `{ [K in keyof Ps]: Ps[K] extends Projection<infer R> ? R : never }` is already written — your job is to produce a value that satisfies it; casting the accumulated array with a type assertion is the one place a cast is justified, but try to avoid it by building the result correctly.
  • In `validateEvent`, narrow the `unknown` step-by-step: first check `typeof raw === 'object' && raw !== null`, then use `'type' in raw` before reading the `type` field — TypeScript won't let you read a property of `unknown` without narrowing first.
  • For `rebuildFromEvents` with a snapshot, remember the snapshot's `version` tells you how many leading events to skip — `events.slice(snapshot.version)` gives you only the unseen tail.

Or clone locally

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