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.

Or clone locally

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