TypeDrop

2026-04-18 Challenge

2026-04-18 Medium

Typed Notification Router

You're building the notification dispatch layer for a multi-channel messaging platform. Raw notification payloads arrive as unknown JSON from a message queue; your router must validate them, fan-out to the correct typed channel handlers, and produce a structured per-recipient delivery report — with zero `any`.

Goals

  • Implement `validatePayload` to narrow `unknown` → `NotificationPayload` with channel-specific field rules, throwing descriptive errors on any violation.
  • Define the `HandlerRegistry` mapped type so each channel key maps to a `ChannelHandler` typed to the exact matching `NotificationPayload` variant via `Extract<>`.
  • Implement `routeNotifications` to validate and fan-out all payloads concurrently with `Promise.allSettled`, capturing both validation and handler errors as `DeliveryFailure`.
  • Implement `createRegistry` as a one-liner using the `satisfies` operator so the caller's object is type-checked against `HandlerRegistry` without widening its inferred type.
challenge.ts
// Core discriminated union — channel is the discriminant
type NotificationPayload =
  | { channel: "email";   to: string; subject: string; body: string }
  | { channel: "sms";     to: string; body: string }
  | { channel: "push";    deviceToken: string; title: string; body: string }
  | { channel: "webhook"; url: string; eventType: string; data: Record<string, string | number | boolean> };

// Generic handler constrained to one variant
type ChannelHandler<T extends NotificationPayload> = (payload: T) => Promise<DeliveryResult>;

// Mapped registry — Extract picks the right variant per key
type HandlerRegistry = {
  [K in NotificationPayload["channel"]]: ChannelHandler<
    Extract<NotificationPayload, { channel: K }>
  >;
};

// Validate unknown → NotificationPayload or throw
function validatePayload(raw: unknown): NotificationPayload { ... }

// Fan-out to handlers concurrently, aggregate into a report
async function routeNotifications(
  rawPayloads: unknown[],
  registry: HandlerRegistry
): Promise<DispatchReport> { ... }
Hints (click to reveal)

Hints

  • For the `HandlerRegistry` mapped type, `[K in NotificationPayload["channel"]]` gives you each channel string as a key — pair it with `Extract<NotificationPayload, { channel: K }>` to get the exact variant.
  • In `validatePayload`, a `switch` on the `channel` field after confirming it's a string lets TypeScript narrow subsequent field checks — no type assertions needed.
  • `Promise.allSettled` returns `PromiseSettledResult<T>[]`; check `.status === 'fulfilled'` vs `'rejected'` to safely extract values or error messages for the report.

Or clone locally

git clone -b challenge/2026-04-18 https://github.com/niltonheck/typedrop.git