TypeDrop

2026-05-08 Challenge

2026-05-08 Easy

Typed Recipe Nutrition Aggregator

You're building the nutrition-analysis layer for a meal-planning app. Raw recipe payloads arrive as `unknown` from a third-party food database API; your engine must validate them, aggregate per-serving nutrition totals, and return a strongly-typed nutrition summary — with zero `any`.

Goals

  • Implement `validateRecipe` to narrow `unknown` payloads into typed `Recipe` objects using a discriminated-union `ValidationError` — no `any` allowed.
  • Implement `aggregateNutrition` to scale per-100g nutrition values by ingredient weight, sum across all ingredients, divide by servings, and build a sorted `topContributors` list.
  • Implement `processRecipes` to run the full validate → skip-invalid → aggregate pipeline over an array of unknown payloads, returning only valid `RecipeSummary` results.
  • Keep all types strict: use the `Result<T, E>` wrapper correctly and ensure TypeScript can narrow both the `ok: true` and `ok: false` branches at every call site.
challenge.ts

// Key types at a glance:

interface NutritionPer100g {
  calories: number; protein: number;
  fat: number; carbs: number; fiber: number;
}
interface Recipe {
  id: string; title: string; servings: number;
  ingredients: Array<{ name: string; grams: number; nutrition: NutritionPer100g }>;
}
interface RecipeSummary {
  id: string; title: string; servings: number;
  perServing: NutritionPer100g;
  topContributors: Array<{ name: string; totalCalories: number; percentOfRecipeCalories: number }>;
}
type ValidationError =
  | { kind: "MISSING_FIELD";  field: string }
  | { kind: "WRONG_TYPE";     field: string; expected: string }
  | { kind: "OUT_OF_RANGE";   field: string; min: number; max: number }
  | { kind: "EMPTY_ARRAY";    field: string };
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

// --- Signatures to implement ---
function validateRecipe(raw: unknown): Result<Recipe, ValidationError> { ... }
function aggregateNutrition(recipe: Recipe): RecipeSummary { ... }
function processRecipes(rawPayloads: unknown[]): RecipeSummary[] { ... }
Hints (click to reveal)

Hints

  • For `validateRecipe`, use `typeof` checks and `Array.isArray` to narrow each field of the `unknown` object — a small helper like `isObject(v: unknown): v is Record<string, unknown>` will prevent repetition.
  • The `Result<T, E>` union narrows automatically: after `if (result.ok)` TypeScript knows `result.value` exists; after `if (!result.ok)` it knows `result.error` exists — rely on that instead of casting.
  • In `aggregateNutrition`, compute each ingredient's scaled calories as `(ingredient.nutrition.calories * ingredient.grams) / 100` before summing, then use `Math.round(value * 100) / 100` (or `parseFloat(value.toFixed(2))`) for consistent 2-decimal rounding.

Or clone locally

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