TypeDrop

2026-03-16 Challenge

2026-03-16 Medium

Typed Notification Preference Engine

You're building the notification settings module for a SaaS platform. Users configure per-channel delivery rules (email, SMS, push), and your engine must validate raw unknown config payloads, merge them with system-level defaults, and produce a resolved, strongly-typed preference map — surfaced through a `Result<T, E>` type so callers can handle every failure mode explicitly.

Goals

  • Define Ok<T>/Err<E> discriminated union and generic ok()/err() constructors, then use them throughout the module.
  • Build ChannelOptionMap, UserPreferences, and ResolvedPreferences using mapped types over the Channel union so the structure is DRY and extensible.
  • Implement parsePreferences to safely narrow an unknown payload field-by-field, returning typed PreferenceError variants on every failure path.
  • Implement mergeWithDefaults and compose it with parsePreferences inside resolvePreferences, then produce a ChannelSummaryMap via describeResolved using the ChannelSummaryKey template literal type.
challenge.ts
// Key types & main function signatures

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

export interface ChannelOptionMap {
  email: { enabled: boolean; frequency: "instant" | "daily" | "weekly" };
  sms:   { enabled: boolean; quietHoursStart: number; quietHoursEnd: number };
  push:  { enabled: boolean; badge: boolean; sound: boolean };
}

// Mapped: every channel optional (user overrides)
export type UserPreferences    = { [C in Channel]?: ChannelOptionMap[C] };
// Mapped: every channel required (defaults filled in)
export type ResolvedPreferences = { [C in Channel]: ChannelOptionMap[C] };

export type PreferenceError =
  | { kind: "validation_error"; field: string; message: string }
  | { kind: "unknown_channel"; channel: string };

export type Result<T, E> = Ok<T> | Err<E>;

// Template literal summary keys: "email_summary" | "sms_summary" | "push_summary"
export type ChannelSummaryKey = `${Channel}_summary`;

export function parsePreferences(raw: unknown): Result<UserPreferences, PreferenceError>;
export function mergeWithDefaults(prefs: UserPreferences): ResolvedPreferences;
export function resolvePreferences(raw: unknown): Result<ResolvedPreferences, PreferenceError>;
export function describeResolved(resolved: ResolvedPreferences): ChannelSummaryMap;
Hints (click to reveal)

Hints

  • A mapped type `{ [C in Channel]?: ChannelOptionMap[C] }` is all you need for UserPreferences — no need to repeat each channel manually.
  • In parsePreferences, narrow the unknown value with `typeof` and `in` checks before reading any property; TypeScript will track the narrowed type for you.
  • Use `satisfies ResolvedPreferences` on SYSTEM_DEFAULTS so you get literal-type inference (e.g. `"daily"`) while still having the type checked at compile time.

Or clone locally

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