TypeDrop

2026-05-05 Challenge

2026-05-05 Medium

Typed Notification Dispatcher

You're building the notification layer for a multi-channel SaaS platform. Raw notification payloads arrive as `unknown` from an internal message bus; your dispatcher must validate them, route each to the correct typed handler via a discriminated union, execute all handlers with a per-channel retry policy, and return a strongly-typed dispatch report — with zero `any`.

Goals

  • Define `NotificationPayload` as a discriminated union and derive `ChannelRegistry` and `HandlerMap` from it using mapped types and `Extract` — no hardcoded string keys.
  • Implement `validatePayload` to narrow `unknown` → `NotificationPayload` with per-channel rules, returning a typed `ValidationResult` discriminated union.
  • Implement `dispatchOne` to validate, route to the correct handler, and retry up to `maxAttempts` times with `delayMs` spacing, returning a `DispatchOutcome`.
  • Implement `dispatchBatch` to concurrently dispatch all payloads via `Promise.all`, capturing every failure as a `DispatchFailure` without ever throwing.
challenge.ts

// Core discriminated union
export type NotificationPayload =
  | EmailPayload | SmsPayload | PushPayload | WebhookPayload;

// Mapped type over the union's discriminant — no hardcoded keys
export type ChannelRegistry = {
  [K in NotificationPayload["channel"]]: RetryPolicy;
};

// Handler map: each channel key → correctly-narrowed handler
export type HandlerMap = {
  [K in NotificationPayload["channel"]]: ChannelHandler<
    Extract<NotificationPayload, { channel: K }>
  >;
};

// Main entry points
export function validatePayload(raw: unknown): ValidationResult { ... }

export async function dispatchOne(
  raw: unknown,
  handlers: HandlerMap,
  registry: ChannelRegistry
): Promise<DispatchOutcome> { ... }

export async function dispatchBatch(
  raws: unknown[],
  handlers: HandlerMap,
  registry: ChannelRegistry
): Promise<DispatchOutcome[]> { ... }
Hints (click to reveal)

Hints

  • For `HandlerMap`, map over `NotificationPayload["channel"]` and use `Extract<NotificationPayload, { channel: K }>` inside the mapped type to get the right payload subtype per key — no union narrowing needed at the call site.
  • In `dispatchOne`, use a `for` loop up to `maxAttempts`, wrap each handler call in try/catch, and `await` a `sleep(delayMs)` between failures; the handler call must be typed through the `HandlerMap` index.
  • In `validatePayload`, first check `typeof raw === 'object' && raw !== null && 'channel' in raw` before narrowing — then use a `switch` on the `channel` field with per-case field checks to keep TypeScript happy without assertions.

Or clone locally

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