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.

Or clone locally

git clone -b challenge/2026-05-21 https://github.com/niltonheck/typedrop.git