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.
Useful resources
Or clone locally
git clone -b challenge/2026-06-26 https://github.com/niltonheck/typedrop.git