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`.
Useful resources
Or clone locally
git clone -b challenge/2026-07-11 https://github.com/niltonheck/typedrop.git