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.

Or clone locally

git clone -b challenge/2026-03-14 https://github.com/niltonheck/typedrop.git