TypeDrop

2026-03-22 Challenge

2026-03-22 Medium

Typed Notification Dispatch Router

You're building the notification layer for a SaaS platform. Users can subscribe to different channels (email, SMS, push), and your router must validate raw subscription configs, fan out typed messages to each channel's handler, and collect a structured per-channel delivery report — surfaced through a `Result<T, E>` type with zero `any`.

Goals

  • Define `Channel`, `NotificationMessage`, and `DispatchError` as discriminated unions with the specified members.
  • Define `DeliveryOutcome` as a mapped type keyed over `Channel["kind"]` and `HandlerRegistry` as a mapped type that narrows each handler to its exact channel and message pair using `Extract<>`.
  • Implement `validateSubscription` to parse an unknown channel array, silently drop invalid entries, and return a typed `Result` with the appropriate `DispatchError` code.
  • Implement `dispatchNotifications` to concurrently invoke per-channel handlers from the registry, record sent/failed/skipped outcomes, and always return `Result<DeliveryOutcome, never>`.
challenge.ts
// Core types and main function signatures

type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

type Channel =
  | { kind: "email"; address: string }
  | { kind: "sms";   phone: string }
  | { kind: "push";  deviceToken: string };

type NotificationMessage =
  | { kind: "email"; subject: string; body: string }
  | { kind: "sms";   text: string }
  | { kind: "push";  title: string; payload: Record<string, string> };

// Mapped over Channel["kind"] — every channel kind gets a delivery slot
type DeliveryOutcome = { [K in Channel["kind"]]: { status: DeliveryStatus; detail: string } };

// Each registry slot is narrowed to the exact Channel + Message pair for kind K
type HandlerRegistry = { [K in Channel["kind"]]: ChannelHandler<Extract<Channel, { kind: K }>, Extract<NotificationMessage, { kind: K }>> };

function validateSubscription(raw: RawSubscription): Result<ValidatedSubscription, DispatchError>;

async function dispatchNotifications(
  subscription: ValidatedSubscription,
  messages: readonly NotificationMessage[],
  registry: HandlerRegistry
): Promise<Result<DeliveryOutcome, never>>;
Hints (click to reveal)

Hints

  • For `HandlerRegistry`, use `Extract<Channel, { kind: K }>` inside a mapped type to get the exact channel variant for each key — this is what gives each handler its precise parameter types.
  • In `validateSubscription`, narrow `raw.channels` step by step: first check `Array.isArray`, then use a type predicate or inline guard to filter each element into a valid `Channel`.
  • In `dispatchNotifications`, build one Promise per channel in the subscription, then pass them all to `Promise.all` — each promise resolves to a `[kind, { status, detail }]` tuple you can assemble into a `DeliveryOutcome`.

Or clone locally

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