TypeDrop
2026-04-01 Challenge
2026-04-01
Medium
Typed Notification Dispatcher
You're building the notification service for a SaaS platform. The system must dispatch typed notifications across multiple channels (email, SMS, push), fan out deliveries concurrently with a configurable limit, and collect a fully typed per-channel Result for every recipient — with zero `any`.
Goals
- Define the three-channel discriminated union `Notification` type with branded `id` and `recipientId` fields.
- Implement `validateNotification` to narrow `unknown → Result<Notification, ValidationError>` with per-field checks.
- Implement `dispatch` using the correct `ChannelSender` selected via a type-safe switch on `notification.channel`.
- Implement `fanOutDispatch` to validate all payloads, fan out dispatches in batches of `config.concurrencyLimit`, and return a fully typed `DispatchSummary`.
challenge.ts
// Key types & main function signature
type Brand<T, B extends string> = T & { readonly __brand: B };
type RecipientId = Brand<string, "RecipientId">;
type NotificationId = Brand<string, "NotificationId">;
type Result<T, E> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly error: E };
type Notification = EmailNotification | SmsNotification | PushNotification;
// Mapped type — your job to fill in the body:
type ChannelSenders = {
[K in Notification["channel"]]: ChannelSender<Extract<Notification, { channel: K }>>;
};
type DispatchConfig = {
readonly concurrencyLimit: number;
readonly senders: ChannelSenders;
};
// Fan out raw payloads → validate → dispatch (concurrency-limited) → summary
async function fanOutDispatch(
rawPayloads: readonly unknown[],
config: DispatchConfig
): Promise<DispatchSummary>;
Hints (click to reveal)
Hints
- For `ChannelSenders`, use a mapped type `{ [K in Notification["channel"]]: ChannelSender<Extract<Notification, { channel: K }>> }` — this lets TypeScript infer the exact sub-type per key.
- In `fanOutDispatch`, slice the validated notifications into chunks of size `concurrencyLimit` and `await Promise.all(chunk.map(...))` per chunk to honour the limit.
- Use a `switch (notification.channel)` with a final `default` branch that calls a `(c: never) => never` helper — this gives you exhaustiveness checking for free.
Useful resources
Or clone locally
git clone -b challenge/2026-04-01 https://github.com/niltonheck/typedrop.git