TypeDrop
2026-05-21 Challenge
2026-05-21
Easy
Typed Recipe Ingredient Scaler
You're building the recipe engine for a meal-planning app. Raw recipe data arrives as `unknown` from a third-party nutrition API; your engine must validate it, scale ingredient quantities to a target serving count, and return a strongly-typed scaled recipe summary — with zero `any`.
Goals
- Define branded, union, and discriminated-union types that model the recipe domain with zero `any`.
- Implement `parseRecipe` to validate an `unknown` payload into a typed `Recipe`, returning the first `ValidationError` encountered.
- Implement `scaleRecipe` to compute per-ingredient `scaledQuantity` values using the `PositiveNumber` brand.
- Compose both functions in `parseAndScale`, including upfront validation that `targetServings` is positive.
challenge.ts
// Key types and main function signatures
export type PositiveNumber = number & { readonly __brand: "PositiveNumber" };
export type Unit = "g" | "ml" | "tsp" | "tbsp" | "cup" | "piece";
export type Ingredient = {
name: string;
quantity: PositiveNumber;
unit: Unit;
};
export type Recipe = {
id: string;
title: string;
servings: PositiveNumber;
ingredients: readonly [Ingredient, ...Ingredient[]]; // non-empty tuple
};
export type ValidationError =
| { kind: "MissingField"; field: string }
| { kind: "InvalidValue"; field: string; reason: string };
export type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
export function parseRecipe(raw: unknown): Result<Recipe, ValidationError>;
export function scaleRecipe(recipe: Recipe, targetServings: PositiveNumber): ScaledRecipe;
export function parseAndScale(raw: unknown, targetServings: number): Result<ScaledRecipe, ValidationError>;
Hints (click to reveal)
Hints
- A branded type is just `number & { readonly __brand: 'PositiveNumber' }` — use a helper function with a runtime `> 0` check to produce one safely.
- For `Unit` validation, a `const VALID_UNITS = new Set([...]) satisfies Set<Unit>` lets you narrow an unknown string in one `has()` call.
- Non-empty tuple `readonly [Ingredient, ...Ingredient[]]` enforces at least one element at the type level — your runtime check (length >= 1) mirrors this guarantee.
Useful resources
Or clone locally
git clone -b challenge/2026-05-21 https://github.com/niltonheck/typedrop.git