DEV Community

albert nahas
albert nahas

Posted on • Originally published at leandine.hashnode.dev

Building a Dietary Preference Engine with TypeScript

Modern consumers are demanding more from their diet apps. It’s no longer enough to list dishes or calorie counts—users expect intelligent food recommendation engines that align with their unique dietary preferences, whether they’re keto, vegan, or following a doctor’s orders for low-sodium meals. Building such a dietary preference engine is both a technical and data challenge, but with TypeScript’s strong typing and expressive modeling, you can create a scalable, robust solution. Let’s walk through the core concepts and practical code for implementing a personalized nutrition engine that can match users to food choices tailored to their goals and needs.

Understanding Dietary Preferences

Dietary preferences are more than just “likes” and “dislikes.” They encompass a wide range of user needs:

  • Ethical or lifestyle choices: vegan, vegetarian, pescatarian, etc.
  • Health goals: keto, paleo, high-protein, low-carb, etc.
  • Medical requirements: low-sodium, gluten-free, nut-free, etc.
  • Taste and cultural factors: spicy, mild, specific cuisines.

A robust food recommendation engine must be able to:

  1. Capture user preferences in a structured way.
  2. Describe dishes accurately with metadata.
  3. Match users to dishes using flexible, extensible logic.
  4. Adapt to new diets or constraints without major rewrites.

Let’s design a TypeScript architecture that meets these goals.

Modeling Dishes and Preferences in TypeScript

Start by defining strong types for both dishes and user preferences. This enables you to catch errors at compile time and maintain clarity as your engine grows.

1. Defining Dietary Tags and Nutritional Data

Create enums and interfaces to capture the most important dietary tags and nutritional facts.

// Common dietary tags
export enum DietaryTag {
  Vegan = 'vegan',
  Vegetarian = 'vegetarian',
  Keto = 'keto',
  LowSodium = 'low-sodium',
  GlutenFree = 'gluten-free',
  NutFree = 'nut-free',
  Paleo = 'paleo',
  DairyFree = 'dairy-free',
  // Extend as needed
}

export interface NutritionInfo {
  calories: number;
  protein: number;     // grams
  carbs: number;       // grams
  fat: number;         // grams
  sodium: number;      // mg
  sugar: number;       // grams
  // Add micronutrients, fiber, etc. as needed
}

export interface Dish {
  id: string;
  name: string;
  tags: DietaryTag[];
  ingredients: string[];
  nutrition: NutritionInfo;
}
Enter fullscreen mode Exit fullscreen mode

This model allows each dish to be tagged (e.g., vegan, keto) and described with precise nutrition data.

2. Modeling User Dietary Preferences

Users may have preferences that are strict (“must be vegan”), flexible (“prefer low sodium”), or goal-oriented (“trying to eat more protein”). Representing this in TypeScript:

export interface DietaryPreference {
  requiredTags?: DietaryTag[];   // Must-have tags
  excludedTags?: DietaryTag[];   // Tags to avoid
  maxNutrition?: Partial<NutritionInfo>; // Upper bounds (e.g., sodium)
  minNutrition?: Partial<NutritionInfo>; // Lower bounds (e.g., protein)
}
Enter fullscreen mode Exit fullscreen mode

Example: a vegan, low-sodium user who wants at least 15g protein per meal:

const examplePreference: DietaryPreference = {
  requiredTags: [DietaryTag.Vegan],
  excludedTags: [],
  maxNutrition: { sodium: 500 },
  minNutrition: { protein: 15 },
};
Enter fullscreen mode Exit fullscreen mode

Matching Dishes to Preferences

The heart of your food recommendation engine is the matching logic. This is where TypeScript’s type safety shines.

1. Tag-Based Filtering

Start by filtering dishes by required and excluded tags:

function matchesTags(dish: Dish, pref: DietaryPreference): boolean {
  const hasRequired = (pref.requiredTags ?? []).every(tag => dish.tags.includes(tag));
  const hasNoExcluded = (pref.excludedTags ?? []).every(tag => !dish.tags.includes(tag));
  return hasRequired && hasNoExcluded;
}
Enter fullscreen mode Exit fullscreen mode

2. Nutrition-Based Filtering

Next, filter based on nutritional constraints:

function matchesNutrition(dish: Dish, pref: DietaryPreference): boolean {
  const { nutrition } = dish;
  const { maxNutrition = {}, minNutrition = {} } = pref;

  for (const key in maxNutrition) {
    if ((nutrition as any)[key] > (maxNutrition as any)[key]) {
      return false;
    }
  }
  for (const key in minNutrition) {
    if ((nutrition as any)[key] < (minNutrition as any)[key]) {
      return false;
    }
  }
  return true;
}
Enter fullscreen mode Exit fullscreen mode

3. Combining Filters

Now, combine these filters into a matcher for your food recommendation engine:

function matchesPreference(dish: Dish, pref: DietaryPreference): boolean {
  return matchesTags(dish, pref) && matchesNutrition(dish, pref);
}
Enter fullscreen mode Exit fullscreen mode

4. Putting It All Together

To recommend dishes, simply filter your menu:

function recommendDishes(menu: Dish[], pref: DietaryPreference): Dish[] {
  return menu.filter(dish => matchesPreference(dish, pref));
}
Enter fullscreen mode Exit fullscreen mode

Handling Fuzzy Preferences and Recommendations

Real-world users may not always have strict preferences. They might want to prefer keto, but not require it, or aim for low sodium but accept a slight overage for a favorite dish. Add scoring and ranking to your dietary preference engine to handle these nuances.

1. Scoring Dishes

Assign scores based on how well a dish matches a user’s goals. For example:

function scoreDish(dish: Dish, pref: DietaryPreference): number {
  let score = 0;

  // Reward required tags
  (pref.requiredTags ?? []).forEach(tag => {
    if (dish.tags.includes(tag)) score += 10;
  });

  // Penalize excluded tags
  (pref.excludedTags ?? []).forEach(tag => {
    if (dish.tags.includes(tag)) score -= 20;
  });

  // Reward nutritional goals (e.g., high protein)
  if (pref.minNutrition?.protein !== undefined) {
    score += Math.min(dish.nutrition.protein, pref.minNutrition.protein) * 2;
  }

  // Penalize excess sodium
  if (pref.maxNutrition?.sodium !== undefined && dish.nutrition.sodium > pref.maxNutrition.sodium) {
    score -= (dish.nutrition.sodium - pref.maxNutrition.sodium) / 10;
  }

  return score;
}
Enter fullscreen mode Exit fullscreen mode

2. Recommending the Best Matches

Return the top-N dishes ordered by score:

function recommendTopDishes(menu: Dish[], pref: DietaryPreference, topN = 5): Dish[] {
  return menu
    .map(dish => ({ dish, score: scoreDish(dish, pref) }))
    .sort((a, b) => b.score - a.score)
    .slice(0, topN)
    .map(entry => entry.dish);
}
Enter fullscreen mode Exit fullscreen mode

This approach provides flexibility; even if there’s no perfect match, you can offer the “next best” options.

Supporting Complex and Evolving Diets

Dietary science and trends evolve rapidly. Your engine should be extensible:

  • Add new tags and nutrients easily: The TypeScript enum and interface model allows this.
  • Support custom user goals: Allow users to define their own tags or nutrition targets.
  • Integrate with external data: Import nutrition facts from APIs or public databases for improved accuracy.
  • Internationalization: Account for regional dish names, ingredient lists, and dietary norms.

Integrating with Your Diet App or Platform

A dietary preference engine is most useful when integrated deeply into your diet app’s user experience. Key integration points include:

  • User onboarding: Ask about dietary restrictions, goals, and allergies.
  • Personalized menu display: Filter and sort dishes in real time based on the user’s profile.
  • Meal planning: Suggest weekly meal plans aligned with user nutrition targets.
  • Feedback loop: Let users rate recommendations, refining future suggestions.

If you’re building for restaurants, health apps, or wellness platforms, there are several ways to implement this logic—whether as a backend service, a client-side module, or a cloud function. Tools like Spoonacular, Edamam, and LeanDine offer APIs or SDKs for nutrition analysis and menu filtering, which can complement your own TypeScript engine.

Key Takeaways

  • Strong typing with TypeScript makes it easier to model dishes, user preferences, and nutritional data, reducing bugs and clarifying intent.
  • Matchmaking logic should combine both tag-based and nutrition-based filtering, with scoring for “fuzzy” matches.
  • Extensibility is essential: design for easy addition of new diets, nutrients, and user goals.
  • User-centric design means capturing preferences in a structured way, integrating recommendations at every point in your diet app.
  • Third-party tools and APIs can help supplement your data and analysis, but the core logic is often best customized for your platform.

A dietary preference engine is the foundation of any modern, personalized nutrition or food recommendation system. By leveraging TypeScript’s capabilities and thoughtful design, you can build an engine that delights users and adapts to the ever-changing world of nutrition science.

Top comments (0)