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