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.

Or clone locally

git clone -b challenge/2026-04-01 https://github.com/niltonheck/typedrop.git