TypeScript theorytheory 0/50 · 0%
Capstone · hard

50. Capstone: a typed on-chain event pipeline

Bringing generics, discriminated unions, and async together.

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.

Check your understanding

  1. 1. What does `ChainEvent["kind"]` extract?

  2. 2. Why is deriving `Record<ChainEvent["kind"], number>` from the union better than writing `Record<"transfer" | "approval" | "mint", number>` by hand?

  3. 3. What does `fetchEvents` returning `Promise<ChainEvent[]>` guarantee to callers?