TypeDrop
2026-06-20 Challenge
2026-06-20
Hard
Typed Event Sourcing Engine with Projection & Snapshot
You're building the event-sourcing backbone for a collaborative document editor. Raw domain events arrive as `unknown` from a Kafka consumer; your engine must validate them, apply them to a strongly-typed aggregate, maintain projections via a registry of typed reducers, and produce versioned snapshots — with zero `any`.
Goals
- Define branded types for AggregateId, EventId, and Version that are mutually incompatible at compile time.
- Build a type-safe ProjectionRegistry using mapped types and Extract<> so each reducer receives the exact DomainEvent variant — not the full union.
- Implement parseRawEvent to safely narrow unknown input to a typed DomainEvent, returning a Result<DomainEvent, ValidationError> with zero type assertions.
- Wire everything together in processRawEventBatch: collect parse errors without halting, replay valid events sequentially, and return a fully-typed BatchResult including an optional snapshot.
challenge.ts
// Key types at a glance:
type AggregateId = string & { readonly __brand: unique symbol };
type Version = number & { readonly __brand: unique symbol };
type DomainEvent =
| { type: "DocumentCreated"; eventId: EventId; aggregateId: AggregateId; version: Version; occurredAt: string; title: string; authorId: string }
| { type: "ContentUpdated"; eventId: EventId; aggregateId: AggregateId; version: Version; occurredAt: string; content: string; wordCount: number }
| { type: "CollaboratorAdded"; eventId: EventId; aggregateId: AggregateId; version: Version; occurredAt: string; collaboratorId: string; role: "viewer" | "editor" }
| { type: "DocumentArchived"; eventId: EventId; aggregateId: AggregateId; version: Version; occurredAt: string; reason: string };
type ProjectionRegistry<S> = {
[K in DomainEvent["type"]]: (state: S, event: Extract<DomainEvent, { type: K }>) => S;
};
// Main pipeline entry point:
function processRawEventBatch(
rawEvents: unknown[],
initial: DocumentState,
registry: ProjectionRegistry<DocumentState>
): BatchResult { /* ... */ }
Hints (click to reveal)
Hints
- For ProjectionRegistry, `[K in DomainEvent["type"]]: (state: S, event: Extract<DomainEvent, { type: K }>) => S` lets TypeScript infer the exact variant in each reducer body.
- When narrowing `unknown` in parseRawEvent, use `typeof` and `in` operator checks — never cast. A helper like `isNonNullObject(v: unknown): v is Record<string, unknown>` keeps the code clean.
- Branded types need a `unique symbol` per brand; declare the symbol inside the type literal itself so each brand is truly distinct even without an exported symbol.
Useful resources
Or clone locally
git clone -b challenge/2026-06-20 https://github.com/niltonheck/typedrop.git