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