TypeDrop
2026-10-05 Challenge
2026-10-05
Medium
Typed Event Store with Aggregation & Snapshot
You're building the core of an event-sourced order management system. Raw domain events are appended to an immutable log, a typed reducer folds them into the current aggregate state, and periodic snapshots are captured — all with the compiler enforcing correct event shapes, state transitions, and snapshot validity.
Goals
- Define a strict discriminated union `OrderEvent` with four variants — each carrying only its own meaningful fields.
- Implement `applyEvent` as a pure, exhaustively type-safe reducer that correctly upserts/removes items and recalculates `totalCents`.
- Create the branded `Snapshot<S>` type and implement `createSnapshot` / `replayFrom` so state can be reconstructed from a checkpoint plus a trailing event slice.
- Implement `getLineItems` to derive a sorted, annotated line-item array from the `Map`-backed items collection.
challenge.ts
// Core domain types & main function signatures
type OrderEvent =
| { kind: "order_placed"; customerId: string; totalCents: number }
| { kind: "item_added"; itemId: string; qty: number; priceCents: number }
| { kind: "item_removed"; itemId: string; qty: number }
| { kind: "order_cancelled"; reason: string };
interface OrderState {
status: "open" | "cancelled";
customerId: string;
items: Map<string, { qty: number; priceCents: number }>;
totalCents: number;
}
type Reducer<S, E> = (state: S, event: E) => S;
// Branded snapshot — plain OrderState cannot be substituted
type Snapshot<S> = S & { readonly __snapshot: true; snapshotAt: number; createdMs: number };
// ── Functions you must implement ─────────────────────────────────
declare function applyEvent(state: OrderState, event: OrderEvent): OrderState;
declare function createSnapshot(state: OrderState, eventIndex: number): Snapshot<OrderState>;
declare function replayFrom(
snapshot: Snapshot<OrderState> | null,
events: OrderEvent[],
reducer: Reducer<OrderState, OrderEvent>
): OrderState;
Hints (click to reveal)
Hints
- A `switch (event.kind)` with one `case` per variant — and NO `default` — forces the compiler to verify exhaustiveness via the `never` type on the implicit final branch.
- For the branded `Snapshot<S>`, intersect `S` with `{ readonly __snapshot: true; snapshotAt: number; createdMs: number }` — the brand field makes it structurally unique so plain states can't slip through.
- When implementing `item_added` upsert, clone the `Map` (e.g. `new Map(state.items)`) before mutating so `applyEvent` remains a pure function.
Useful resources
Or clone locally
git clone -b challenge/2026-10-05 https://github.com/niltonheck/typedrop.git