TypeScript Tutorial: Build a Simple Interactive Recipe App

Are you tired of flipping through cookbooks or scrolling endlessly online to find your favorite recipes? Wouldn’t it be great to have a digital recipe book that’s not only accessible but also interactive, allowing you to easily search, add, and organize your culinary creations? In this tutorial, we’ll dive into building a simple, yet functional, interactive recipe application using TypeScript. This project is perfect for beginners to intermediate developers looking to sharpen their skills and learn practical TypeScript applications.

Why Build a Recipe App with TypeScript?

TypeScript, a superset of JavaScript, brings static typing to your JavaScript code. This means you can catch errors early in development, improve code readability, and enhance maintainability. Building a recipe app provides an excellent opportunity to practice these benefits. You’ll learn how to define data structures, manage user input, and implement search and filtering functionalities – all within a type-safe environment.

Setting Up Your Project

Before we start coding, let’s set up our development environment. You’ll need Node.js and npm (Node Package Manager) installed on your system. If you don’t have them, download and install them from the official Node.js website. Once installed, create a new project directory and initialize a Node.js project:

mkdir recipe-app
cd recipe-app
npm init -y

Next, install TypeScript globally or locally within your project. We’ll opt for a local installation for this tutorial:

npm install typescript --save-dev

Now, let’s initialize a TypeScript configuration file, tsconfig.json. This file tells the TypeScript compiler how to compile your code. Run the following command:

npx tsc --init

This command creates a tsconfig.json file with default settings. You can customize these settings to fit your project needs. For this tutorial, we’ll keep the default settings, but you might want to adjust the output directory (outDir) and the target JavaScript version (target) in a real-world project.

Defining Data Structures

The foundation of our recipe app is the data. We’ll define interfaces to represent a recipe and its components. Create a new file called recipe.ts and add the following code:


// recipe.ts

interface Ingredient {
  name: string;
  quantity: number;
  unit: string;
}

interface Recipe {
  id: number;
  name: string;
  description: string;
  ingredients: Ingredient[];
  instructions: string[];
  cuisine: string;
  prepTime: number; // in minutes
  cookTime: number; // in minutes
  imageUrl?: string; // Optional image URL
}

export { Ingredient, Recipe };

Let’s break down this code:

  • Ingredient interface: Defines the structure for each ingredient, including its name, quantity, and unit (e.g., “cup”, “grams”).
  • Recipe interface: Defines the structure for a recipe, including its ID, name, description, ingredients (an array of Ingredient objects), instructions (an array of strings), cuisine, prep time, cook time, and an optional image URL.

These interfaces provide a clear blueprint for our data, ensuring type safety throughout our application.

Creating Recipe Data

Now, let’s create some sample recipe data to populate our application. Create a file named data.ts and add the following code:


// data.ts
import { Recipe } from './recipe';

const recipes: Recipe[] = [
  {
    id: 1,
    name: 'Spaghetti Carbonara',
    description: 'Classic Italian pasta dish.',
    ingredients: [
      { name: 'Spaghetti', quantity: 200, unit: 'g' },
      { name: 'Eggs', quantity: 2, unit: '' },
      { name: 'Pancetta', quantity: 100, unit: 'g' },
      { name: 'Parmesan cheese', quantity: 50, unit: 'g' },
      { name: 'Black pepper', quantity: 1, unit: 'tsp' }
    ],
    instructions: [
      'Cook spaghetti according to package directions.',
      'Fry pancetta until crispy.',
      'Whisk eggs with parmesan and pepper.',
      'Combine spaghetti, pancetta, and egg mixture.',
      'Serve immediately.'
    ],
    cuisine: 'Italian',
    prepTime: 10,
    cookTime: 15,
    imageUrl: 'https://example.com/carbonara.jpg'
  },
  {
    id: 2,
    name: 'Chicken Stir-Fry',
    description: 'Quick and easy stir-fry recipe.',
    ingredients: [
      { name: 'Chicken breast', quantity: 200, unit: 'g' },
      { name: 'Soy sauce', quantity: 2, unit: 'tbsp' },
      { name: 'Broccoli', quantity: 100, unit: 'g' },
      { name: 'Rice', quantity: 150, unit: 'g' },
    ],
    instructions: [
      'Cut chicken into pieces.',
      'Stir-fry chicken and vegetables.',
      'Add soy sauce.',
      'Serve with rice.'
    ],
    cuisine: 'Asian',
    prepTime: 15,
    cookTime: 20,
    imageUrl: 'https://example.com/stirfry.jpg'
  },
  {
    id: 3,
    name: 'Chocolate Chip Cookies',
    description: 'Classic homemade cookies.',
    ingredients: [
      { name: 'Flour', quantity: 200, unit: 'g' },
      { name: 'Butter', quantity: 100, unit: 'g' },
      { name: 'Sugar', quantity: 100, unit: 'g' },
      { name: 'Chocolate chips', quantity: 150, unit: 'g' },
    ],
    instructions: [
      'Cream butter and sugar.',
      'Add flour and chocolate chips.',
      'Bake at 350F for 10 minutes.',
    ],
    cuisine: 'American',
    prepTime: 20,
    cookTime: 10,
    imageUrl: 'https://example.com/cookies.jpg'
  }
];

export { recipes };

This file defines an array of Recipe objects. Each object contains the details of a recipe, adhering to the Recipe interface we defined earlier. The imageUrl property is an optional field, as indicated by the question mark in the interface definition. This is useful if you don’t always have an image for every recipe.

Creating the Recipe App Logic

Now, let’s create the core logic for our recipe app. We’ll create a file called app.ts. This file will handle fetching the data (from our data.ts file), rendering the recipes, and implementing search functionality.


// app.ts
import { recipes } from './data';
import { Recipe } from './recipe';

function displayRecipes(recipesToDisplay: Recipe[]): void {
  const recipeContainer = document.getElementById('recipe-container');
  if (!recipeContainer) {
    console.error('Recipe container not found in the DOM.');
    return;
  }

  recipeContainer.innerHTML = ''; // Clear existing content

  recipesToDisplay.forEach(recipe => {
    const recipeElement = document.createElement('div');
    recipeElement.classList.add('recipe-card');

    recipeElement.innerHTML = `
      <img src="${recipe.imageUrl || 'default-image.jpg'}" alt="${recipe.name}" />
      <h3>${recipe.name}</h3>
      <p><b>Cuisine:</b> ${recipe.cuisine}</p>
      <p><b>Prep Time:</b> ${recipe.prepTime} mins | <b>Cook Time:</b> ${recipe.cookTime} mins</p>
      <p>${recipe.description}</p>
      <button class="view-recipe" data-recipe-id="${recipe.id}">View Recipe</button>
    `;

    recipeContainer.appendChild(recipeElement);
  });
}

function searchRecipes(searchTerm: string): Recipe[] {
  const searchTermLower = searchTerm.toLowerCase();
  return recipes.filter(recipe =>
    recipe.name.toLowerCase().includes(searchTermLower) ||
    recipe.description.toLowerCase().includes(searchTermLower) ||
    recipe.cuisine.toLowerCase().includes(searchTermLower)
  );
}

function setupSearch() {
  const searchInput = document.getElementById('search-input') as HTMLInputElement;
  const searchButton = document.getElementById('search-button') as HTMLButtonElement;

  if (!searchInput || !searchButton) {
    console.error('Search input or button not found in the DOM.');
    return;
  }

  searchButton.addEventListener('click', () => {
    const searchTerm = searchInput.value;
    const filteredRecipes = searchRecipes(searchTerm);
    displayRecipes(filteredRecipes);
  });
}

// Initial display of all recipes
displayRecipes(recipes);
setupSearch();

Let’s break down this code:

  • Import Statements: We import the recipes array from data.ts and the Recipe interface from recipe.ts.
  • displayRecipes(recipesToDisplay: Recipe[]): void: This function takes an array of Recipe objects and renders them to the DOM. It iterates through the recipes and creates HTML elements for each recipe, including an image, title, description, and a “View Recipe” button. It also handles the case where the recipe container is not found in the DOM, preventing errors.
  • searchRecipes(searchTerm: string): Recipe[]: This function filters the recipes based on a search term. It converts the search term and recipe details to lowercase for case-insensitive searching.
  • setupSearch(): void: This function sets up the search functionality. It gets the search input and button elements from the DOM. It then attaches an event listener to the search button. When the button is clicked, it retrieves the search term from the input field, calls searchRecipes to filter the recipes, and then calls displayRecipes to display the filtered results.
  • Initial Display and Setup: Finally, the code calls displayRecipes(recipes) to initially display all recipes when the page loads, and setupSearch() to initialize the search functionality.

Building the HTML Structure

Now, let’s create the HTML file (index.html) to structure our application. This file will contain the necessary HTML elements for displaying the recipes and the search functionality. Create an index.html file in your project root and add the following code:


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Recipe App</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h1>Recipe App</h1>
  <div class="search-container">
    <input type="text" id="search-input" placeholder="Search recipes...">
    <button id="search-button">Search</button>
  </div>
  <div id="recipe-container">
    <!-- Recipes will be displayed here -->
  </div>
  <script src="app.js"></script>
</body>
</html>

This HTML file includes:

  • A basic HTML structure with a title and a viewport meta tag.
  • A link to a CSS file (style.css) for styling the app.
  • A search input field and a search button.
  • A div with the ID recipe-container, where the recipes will be displayed.
  • A script tag that includes app.js, which will contain our compiled TypeScript code.

Adding Styles with CSS

To make our recipe app visually appealing, let’s add some CSS. Create a file named style.css in your project root and add the following CSS rules:


body {
  font-family: sans-serif;
  margin: 20px;
}

h1 {
  text-align: center;
}

.search-container {
  margin-bottom: 20px;
  text-align: center;
}

#search-input {
  padding: 10px;
  width: 300px;
  border: 1px solid #ccc;
  border-radius: 4px;
  margin-right: 10px;
}

#search-button {
  padding: 10px 20px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.recipe-card {
  border: 1px solid #ddd;
  padding: 10px;
  margin-bottom: 15px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.recipe-card img {
  width: 100%;
  max-height: 200px;
  object-fit: cover;
  border-radius: 8px;
  margin-bottom: 10px;
}

.view-recipe {
  background-color: #008CBA;
  color: white;
  padding: 10px 15px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  display: block;
  margin-top: 10px;
  text-align: center;
}

This CSS provides basic styling for the body, headings, search input, search button, and recipe cards. Feel free to customize the styles to your liking.

Compiling and Running the App

Now that we’ve written our TypeScript code, we need to compile it into JavaScript. Open your terminal in the project directory and run the following command:

npx tsc

This command will compile all TypeScript files (.ts) in your project and generate corresponding JavaScript files (.js) in the same directory. If you have configured an outDir in your tsconfig.json, the compiled JavaScript files will be placed in that directory instead.

Finally, open index.html in your web browser. You should see the recipe app with the sample recipes displayed and the search functionality working. If you encounter any errors, check the browser’s developer console for error messages and review your code.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to fix them:

  • Incorrect File Paths: Double-check that your file paths in the import statements and the <script src="app.js"></script> tag in index.html are correct. Incorrect paths will prevent the code from running.
  • Typos: TypeScript can catch many errors, but typos in variable names, function names, or property names can still occur. Carefully review your code for typos, especially when referencing properties of objects.
  • DOM Element Selection Issues: If you are unable to find the HTML elements in the DOM, make sure your element IDs in the HTML match the IDs you are targeting in your TypeScript code (e.g., document.getElementById('search-input')). Also, ensure that your JavaScript code is executed after the DOM is fully loaded. You can use an event listener to ensure that your JavaScript runs after the page has loaded:

document.addEventListener('DOMContentLoaded', () => {
  // Your code here, including displayRecipes and setupSearch
  displayRecipes(recipes);
  setupSearch();
});
  • Incorrect Data Types: TypeScript’s strong typing can prevent some errors, but ensure that you are using the correct data types. If you’re getting unexpected results, check the data types of variables and function parameters.
  • Compiler Errors: The TypeScript compiler will give you helpful error messages. Read these messages carefully, as they will often point you directly to the source of the problem.
  • Key Takeaways

    This tutorial has provided a solid foundation for building an interactive recipe app with TypeScript. You’ve learned how to define interfaces for data structures, manage user input, implement search and filtering functionalities, and integrate your TypeScript code with HTML. You’ve also gained practical experience with essential TypeScript concepts, such as type annotations, interfaces, and DOM manipulation. This hands-on project gives you the tools and understanding to create more complex and feature-rich web applications.

    FAQ

    Here are some frequently asked questions:

    1. Can I add more features to the recipe app?

      Yes, absolutely! You can extend the app by adding features like adding new recipes, editing existing recipes, deleting recipes, sorting recipes, and implementing more advanced search and filtering options. You could also incorporate user authentication and a database to store and retrieve recipes.

    2. How can I handle images in the recipes?

      You can store image URLs in the Recipe interface and display the images using the <img> tag. Consider using a service like Cloudinary or Imgur to host your images for better performance and management.

    3. Can I use a framework like React or Angular with TypeScript?

      Yes, TypeScript works seamlessly with popular JavaScript frameworks like React, Angular, and Vue.js. In fact, Angular is built with TypeScript. Using TypeScript with these frameworks can significantly improve code quality and maintainability.

    4. How can I deploy this app?

      You can deploy this app using services like Netlify, Vercel, or GitHub Pages. These services allow you to host static websites easily. You’ll need to compile your TypeScript code to JavaScript and upload the HTML, CSS, and JavaScript files to the service.

    The journey into TypeScript doesn’t end here. As you work on more projects, you’ll discover even more of its power. Embrace the type-safety, readability, and maintainability that TypeScript offers, and continue to explore its capabilities. With each project, your skills will grow, and you’ll become more confident in your ability to build robust and scalable web applications. Keep practicing, experimenting, and building – the possibilities are endless!