TypeDrop

2026-10-03 Challenge

2026-10-03 Easy

Typed Notification Router

You're building the notification dispatch layer for a team productivity app. The system receives raw notification events from various channels (email, push, SMS), routes each one to the correct typed handler, and produces a structured delivery summary — all enforced by the compiler with no unsafe casts.

Goals

  • Define a discriminated union `NotificationEvent` with three channel-specific variants (EmailEvent, PushEvent, SmsEvent).
  • Build `ChannelEventMap` and `HandlerMap` mapped types so each channel key is paired with a handler that receives only that channel's event shape.
  • Implement `createRouter` so it narrows on `event.channel` and delegates to the correct typed handler, returning a `DeliveryResult`.
  • Implement `summarise` using a single `Array.prototype.reduce` call to produce totals, per-status counts, and per-channel groupings.
challenge.ts
// Key types & main function signatures

type Channel = "email" | "push" | "sms";

type EmailEvent = { channel: "email"; to: string; subject: string; body: string };
type PushEvent  = { channel: "push";  deviceToken: string; title: string; payload: Record<string, string> };
type SmsEvent   = { channel: "sms";   phoneNumber: string; message: string };

type NotificationEvent = EmailEvent | PushEvent | SmsEvent;

// Maps each Channel key → its specific event type
type ChannelEventMap = { /* TODO */ };

// Each handler receives the *exact* event type for its channel
type HandlerMap = { /* TODO */ };

// Registers handlers and returns a type-safe router
function createRouter(handlers: HandlerMap): {
  route(event: NotificationEvent): DeliveryResult;
};

// Single-pass reduce over results → structured summary
function summarise(results: DeliveryResult[]): DeliverySummary;
Hints (click to reveal)

Hints

  • A mapped type over `Channel` with a conditional type on the value side (e.g. `K extends 'email' ? EmailEvent : ...`) is exactly what `ChannelEventMap` needs.
  • Inside `createRouter`, a `switch (event.channel)` block gives TypeScript enough information to narrow `event` to the exact variant — no casts needed.
  • For `summarise`, initialise the accumulator with all three `DeliveryStatus` keys set to 0 so `counts` is always a complete `Record<DeliveryStatus, number>`.

Or clone locally

git clone -b challenge/2026-10-03 https://github.com/niltonheck/typedrop.git