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`.
Useful resources
Or clone locally
git clone -b challenge/2026-03-22 https://github.com/niltonheck/typedrop.git