A realistic capstone combines everything: a discriminated union of decoded events, a generic typed emitter, async fetching, and utility types for transforming the data — mirroring how a real indexing/monitoring service might be structured.
ts
type ChainEvent =
| { kind: "transfer"; from: string; to: string; amount: bigint }
| { kind: "approval"; owner: string; spender: string; amount: bigint }
| { kind: "mint"; to: string; amount: bigint };
async function fetchEvents(fromBlock: number): Promise<ChainEvent[]> {
// pretend RPC call
return [
{ kind: "transfer", from: "0xa", to: "0xb", amount: 100n },
{ kind: "mint", to: "0xc", amount: 50n },
];
}
function summarize(events: ChainEvent[]): Record<ChainEvent["kind"], number> {
const summary: Record<ChainEvent["kind"], number> = { transfer: 0, approval: 0, mint: 0 };
for (const e of events) summary[e.kind] += 1;
return summary;
}
async function run() {
const events = await fetchEvents(0);
const summary = summarize(events);
console.log(summary.transfer, summary.mint);
}`ChainEvent["kind"]` extracts the union of literal kinds directly from the event union itself, so `Record<ChainEvent["kind"], number>` stays automatically in sync if a new event variant is ever added — a small but powerful demonstration of deriving types instead of duplicating them.