TypeDrop
2026-04-11 Challenge
2026-04-11
Hard
Typed Event Sourcing Ledger
You're building the audit-log replay engine for a fintech platform. Raw domain events arrive as unknown JSON streams; your engine must validate them, fold them into a strongly-typed account ledger via a discriminated-union event bus, and expose a typed projection API — with zero `any`.
Goals
- Define all branded types, discriminated-union events, and a generic Result<T,E> type with zero `any`.
- Implement fail-fast runtime validators that narrow `unknown` inputs to fully-typed `DomainEvent` values.
- Implement `applyEvent` with exhaustive discriminated-union matching and business-rule enforcement (e.g., no overdrafts).
- Implement `buildLedger` and `summariseLedger` — the latter must aggregate all fields in a single pass over the accounts Map.
challenge.ts
// Key types + main function signatures at a glance
export type EventId = string & { readonly _brand: "EventId" };
export type AccountId = string & { readonly _brand: "AccountId" };
export type DomainEvent =
| AccountOpened | MoneyDeposited | MoneyWithdrawn | AccountClosed;
export type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
export type AccountProjection = {
accountId: AccountId;
ownerName: string;
balance: number;
status: "open" | "closed";
eventCount: number;
openedAt: string;
closedAt: string | null;
};
// Parse a raw unknown blob → typed DomainEvent or ValidationError
export function parseDomainEvent(raw: unknown): Result<DomainEvent, ValidationError>;
// Fold one event onto a projection (or null for first event)
export function applyEvent(
projection: AccountProjection | null,
event: DomainEvent
): Result<AccountProjection, ValidationError>;
// Process a whole stream, collect projections + errors
export function buildLedger(rawEvents: unknown[]): LedgerReport;
// Single-pass aggregate over all projections
export function summariseLedger(report: LedgerReport): LedgerSummary;
Hints (click to reveal)
Hints
- For branded types, an intersection like `string & { readonly _brand: "EventId" }` lets you keep string assignability while blocking accidental mixing — use a helper cast only in your validators where you've already confirmed the raw value is a string.
- In `applyEvent`, a `switch (event.type)` over the discriminated union gives you exhaustive narrowing for free — TypeScript will tell you if you miss a branch.
- For the single-pass `summariseLedger`, use `Array.from(report.accounts.values()).reduce(...)` and accumulate all fields (open count, closed count, total balance, max balance, most-active ID) in one reducer accumulator object.
Useful resources
Or clone locally
git clone -b challenge/2026-04-11 https://github.com/niltonheck/typedrop.git