TypeDrop

2026-06-13 Challenge

2026-06-13 Medium

Typed Event Sourcing Ledger Reconstructor

You're building the read-model layer for a fintech platform that uses event sourcing. Raw domain events arrive as `unknown` from a Kafka consumer; your reconstructor must validate them, replay them in order against a typed ledger state machine, and produce a strongly-typed account snapshot — with zero `any`.

Goals

  • Define a `LedgerEvent` discriminated union and `AccountSnapshot` interface with branded ID types, ensuring no plain strings can be used as `AccountId` or `TransactionId`.
  • Implement `parseLedgerEvent` that narrows `unknown` input to a typed `LedgerEvent` or returns a descriptive `ValidationError` — using no `any` or unsafe casts.
  • Implement `replayEvents` that sorts events by `occurredAt`, drives a typed state machine, and returns either an `AccountSnapshot` or a specific `ReplayError` for every invalid transition.
  • Wire everything together in `reconstructLedger`, separating parse failures from replay failures into a strongly-typed `LedgerReport`.
challenge.ts
// Key types & main pipeline signature

export type AccountId     = string & { readonly __brand: "AccountId" };
export type TransactionId = string & { readonly __brand: "TransactionId" };

export type LedgerEvent =
  | { type: "account_opened";  accountId: AccountId; occurredAt: number; owner: string; initialBalance: number }
  | { type: "funds_deposited"; accountId: AccountId; occurredAt: number; transactionId: TransactionId; amount: number }
  | { type: "funds_withdrawn"; accountId: AccountId; occurredAt: number; transactionId: TransactionId; amount: number }
  | { type: "account_closed";  accountId: AccountId; occurredAt: number; reason: string };

export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

export type LedgerReport = {
  snapshot:    AccountSnapshot | null;
  parseErrors: ValidationError[];
  replayError: ReplayError | null;
};

// Validate raw unknown → typed event
export function parseLedgerEvent(raw: unknown): Result<LedgerEvent, ValidationError>;

// Replay sorted events into a snapshot
export function replayEvents(events: ReadonlyArray<LedgerEvent>): Result<AccountSnapshot, ReplayError>;

// Full pipeline: parse all raws, replay valid events, return report
export function reconstructLedger(rawEvents: ReadonlyArray<unknown>): LedgerReport;
Hints (click to reveal)

Hints

  • A discriminated union's exhaustiveness is only guaranteed at compile time if you add a `never`-typed default branch — use that to catch missed event variants in `replayEvents`.
  • For `parseLedgerEvent`, check `typeof value === 'object' && value !== null` first, then use `'fieldName' in value` guards before accessing any property.
  • The `brand<T>()` helper is already provided — use it only when you've already validated the raw string, converting it to `AccountId` or `TransactionId` safely.

Or clone locally

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