TypeDrop

2026-07-11 Challenge

2026-07-11 Easy

Typed User Notification Preferences Merger

You're building the notification settings module for a SaaS platform. Users can configure per-channel preferences (email, SMS, push), and admins can define org-level defaults. Your module must validate raw preference objects arriving as `unknown`, merge user overrides on top of org defaults, and return a fully-typed `ResolvedPreferences` record — with zero `any`.

Goals

  • Define `PreferencesMap` as a mapped type over `Channel` so every channel key is required and typed as `ChannelConfig`.
  • Implement `isFrequency` and `isChannelConfig` as type guards that narrow `unknown` to the correct types without `any` or casting.
  • Implement `parsePartialPreferences` to safely extract only valid channel entries from an `unknown` blob, returning `Partial<PreferencesMap>`.
  • Implement `mergePreferences` to layer user overrides on top of org defaults and return a fully-typed `ResolvedPreferences` with sorted `overriddenChannels`.
challenge.ts
/** The three supported notification channels. */
type Channel = "email" | "sms" | "push";

/** Delivery frequency for a channel. */
type Frequency = "immediate" | "digest" | "off";

type ChannelConfig = { frequency: Frequency; preview: boolean };

/** Every channel MUST be present — use a mapped type over Channel. */
type PreferencesMap = unknown; // TODO: { [K in Channel]: ChannelConfig }

type ResolvedPreferences = {
  userId: string;
  preferences: PreferencesMap;
  overriddenChannels: Channel[];
  mergedAt: string;
};

// Your main function to implement:
function mergePreferences(
  userId: string,
  orgDefaults: PreferencesMap,
  userRaw: unknown
): ResolvedPreferences { /* TODO */ throw new Error("Not implemented"); }
Hints (click to reveal)

Hints

  • A mapped type `{ [K in Channel]: ChannelConfig }` is exactly equivalent to a `Record<Channel, ChannelConfig>` — either form works for `PreferencesMap`.
  • To check if a string is a valid `Channel` inside `parsePartialPreferences`, define a `CHANNELS` constant (`const CHANNELS = ['email', 'sms', 'push'] as const`) and use `.includes()` — TypeScript will need a small cast-free helper or a type predicate.
  • The `satisfies` operator (TS 4.9+) lets you annotate `PLATFORM_DEFAULTS` against `PreferencesMap` while keeping the narrower literal types inferred — write `= { ... } satisfies PreferencesMap`.

Or clone locally

git clone -b challenge/2026-07-11 https://github.com/niltonheck/typedrop.git