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