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