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