TypeDrop
2026-03-14 Challenge
2026-03-14
Easy
Typed Recipe Ingredient Scaler
You're building the recipe feature for a meal-planning app. Users can scale any recipe up or down by a multiplier, and your job is to parse raw unknown ingredient data, convert quantities to a common unit system, and return a fully typed scaled ingredient list — with zero `any`.
Goals
- Define `VolumeUnit`, `WeightUnit`, `CountUnit`, and a discriminated `Unit` union, plus a generic `Result<T,E>` and a discriminated `ValidationError` union.
- Implement `parseIngredient` to validate an `unknown` value into a typed `Ingredient`, returning the correct `ValidationError` variant on any bad field.
- Populate the `Record<VolumeUnit, number>` and `Record<WeightUnit, number>` conversion tables, then use them inside `scaleRecipe` to convert and scale each ingredient to its canonical unit.
- Implement `scaleRecipe` to short-circuit on the first parse failure and return a fully typed `Result<ScaledIngredient[], ValidationError>`.
challenge.ts
// Key types and main function signatures
export type VolumeUnit = "tsp" | "tbsp" | "cup" | "ml" | "l";
export type WeightUnit = "g" | "kg" | "oz" | "lb";
export type CountUnit = "whole" | "pinch" | "slice";
export type Unit =
| { kind: "volume"; unit: VolumeUnit }
| { kind: "weight"; unit: WeightUnit }
| { kind: "count"; unit: CountUnit };
export type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
export interface ScaledIngredient {
name: string;
scaledQuantity: number; // multiplied & converted to canonical unit
canonicalUnit: string; // "ml" | "g" | original CountUnit string
note?: string;
}
// Parse one raw unknown value → typed Ingredient or ValidationError
export function parseIngredient(raw: unknown): Result<Ingredient, ValidationError>;
// Validate + scale an entire list; short-circuits on first bad entry
export function scaleRecipe(
rawIngredients: unknown[],
multiplier: number
): Result<ScaledIngredient[], ValidationError>;
Hints (click to reveal)
Hints
- A `Record<VolumeUnit, number>` will cause a compile error if you forget any member of the union — use that to your advantage when filling in the conversion tables.
- Inside `scaleRecipe`, narrow `ingredient.unit.kind` with an `if`/`switch` to access the correct sub-type — TypeScript will enforce exhaustiveness and give you the right `unit` field in each branch.
- For `parseIngredient`, build small typed constant arrays like `const VOLUME_UNITS: VolumeUnit[] = [...]` and use `Array.prototype.includes` with a type predicate to validate `unitValue` without casting.
Useful resources
Or clone locally
git clone -b challenge/2026-03-14 https://github.com/niltonheck/typedrop.git