TypeScript Tutorial: Building a Simple Web-Based Expense Tracker

Managing finances can be a daunting task. Tracking income and expenses, budgeting, and understanding where your money goes often feels complex and time-consuming. Imagine having a simple, intuitive tool that helps you visualize your spending habits, set financial goals, and stay on top of your budget without the need for complicated spreadsheets or expensive software. This tutorial will guide you through building a basic web-based expense tracker using TypeScript, empowering you to take control of your finances with a practical, hands-on project.

Why Build an Expense Tracker?

Building an expense tracker is more than just a coding exercise; it’s a valuable learning experience. It allows you to:

  • Learn practical TypeScript: You’ll apply fundamental TypeScript concepts like types, interfaces, classes, and modules in a real-world scenario.
  • Understand web development: You’ll gain experience with HTML, CSS, and JavaScript, the building blocks of web applications.
  • Improve financial literacy: You’ll deepen your understanding of how to manage your own finances by creating a tool designed for that purpose.
  • Build a portfolio project: A functional expense tracker is a great addition to your portfolio, showcasing your skills to potential employers or clients.

This tutorial is designed for beginners to intermediate developers. We’ll break down the project into manageable steps, explaining each concept in simple language with clear examples. You’ll not only learn how to build the application but also understand the underlying principles.

Project Setup

Before we dive into coding, let’s set up our development environment. You’ll need:

  • Node.js and npm (Node Package Manager): These are essential for managing project dependencies and running the TypeScript compiler. Download them from nodejs.org.
  • A code editor: Visual Studio Code (VS Code) is highly recommended due to its excellent TypeScript support, but you can use any editor you prefer.
  • Basic understanding of HTML, CSS, and JavaScript: While this tutorial focuses on TypeScript, familiarity with these technologies will be helpful.

Let’s create a new project directory and initialize it with npm:

mkdir expense-tracker
cd expense-tracker
npm init -y

This will create a `package.json` file in your project directory. Next, install TypeScript and a few helpful packages:

npm install typescript --save-dev
npm install --save-dev @types/node

`typescript` is the TypeScript compiler, and `@types/node` provides type definitions for Node.js. Now, create a `tsconfig.json` file in your project directory. This file configures the TypeScript compiler. A basic configuration looks like this:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

Let’s break down these options:

  • `”target”: “es5″`: Specifies the JavaScript version to compile to.
  • `”module”: “commonjs”`: Specifies the module system to use.
  • `”outDir”: “./dist”`: Specifies the output directory for compiled JavaScript files.
  • `”rootDir”: “./src”`: Specifies the root directory of your TypeScript source files.
  • `”strict”: true`: Enables strict type checking.
  • `”esModuleInterop”: true`: Enables interoperability between CommonJS and ES modules.
  • `”skipLibCheck”: true`: Skips type checking of declaration files (*.d.ts).
  • `”forceConsistentCasingInFileNames”: true`: Enforces consistent casing in file names.
  • `”include”: [“src/**/*”]`: Specifies which files to include in the compilation.

Create a `src` directory to hold your TypeScript files. You can then create an `index.ts` file inside the `src` directory. This will be the entry point for your application. Finally, add a script to your `package.json` to compile your TypeScript code:

"scripts": {
  "build": "tsc"
}

Now, you can run `npm run build` to compile your TypeScript code.

Core Data Structures

Let’s define the data structures for our expense tracker. We’ll use TypeScript interfaces to define the shape of our data. Create a file named `src/types.ts` and add the following:

// src/types.ts
export interface Expense {
  id: number;
  description: string;
  amount: number;
  date: string; // YYYY-MM-DD
  category: string;
}

export interface Category {
  id: number;
  name: string;
}

Here, we define two interfaces: `Expense` and `Category`. The `Expense` interface represents a single expense, and the `Category` interface represents a category for expenses. These interfaces clearly define the structure of our data, making our code more readable and maintainable. The `date` property uses a string in the format YYYY-MM-DD for simplicity; you could use a `Date` object if you prefer.

Implementing the Expense Tracker Logic

Now, let’s write the core logic for our expense tracker. In `src/index.ts`, we’ll create classes and functions to manage expenses. Initially, we will focus on the data model and basic operations. We’ll add the user interface (UI) later.

// src/index.ts
import { Expense, Category } from './types';

class ExpenseTracker {
  private expenses: Expense[] = [];
  private categories: Category[] = [
    { id: 1, name: 'Food' },
    { id: 2, name: 'Transportation' },
    { id: 3, name: 'Housing' },
    { id: 4, name: 'Entertainment' },
    { id: 5, name: 'Utilities' },
  ];
  private nextExpenseId: number = 1;

  addExpense(description: string, amount: number, date: string, categoryId: number): void {
    const category = this.categories.find(c => c.id === categoryId);
    if (!category) {
      console.error('Invalid category ID');
      return;
    }

    const newExpense: Expense = {
      id: this.nextExpenseId++,
      description,
      amount,
      date,
      category: category.name,
    };
    this.expenses.push(newExpense);
    console.log('Expense added:', newExpense);
  }

  getExpenses(): Expense[] {
    return this.expenses;
  }

  getCategories(): Category[] {
    return this.categories;
  }

  getTotalExpenses(): number {
    return this.expenses.reduce((sum, expense) => sum + expense.amount, 0);
  }

  getExpensesByCategory(categoryId: number): Expense[] {
    const category = this.categories.find(c => c.id === categoryId);
    if (!category) {
      console.error('Invalid category ID');
      return [];
    }
    return this.expenses.filter(expense => expense.category === category.name);
  }
}

// Example Usage
const tracker = new ExpenseTracker();

tracker.addExpense('Groceries', 50, '2024-01-20', 1);
tracker.addExpense('Gas', 40, '2024-01-20', 2);

console.log('All Expenses:', tracker.getExpenses());
console.log('Total Expenses:', tracker.getTotalExpenses());
console.log('Expenses by Category (Food):', tracker.getExpensesByCategory(1));

Let’s break down this code:

  • `ExpenseTracker` class: This class encapsulates all the logic related to managing expenses.
  • `expenses` array: This private array stores our expense data.
  • `categories` array: This private array stores predefined expense categories.
  • `nextExpenseId`: A private variable to generate unique IDs for expenses.
  • `addExpense()` method: Adds a new expense to the `expenses` array. It takes a description, amount, date, and category ID as input. It also performs basic validation to ensure a valid category ID is provided.
  • `getExpenses()` method: Returns all expenses.
  • `getCategories()` method: Returns all categories.
  • `getTotalExpenses()` method: Calculates the total expenses.
  • `getExpensesByCategory()` method: Filters and returns expenses by a given category ID.
  • Example Usage: Demonstrates how to create an `ExpenseTracker` instance, add expenses, and retrieve data.

This code provides a solid foundation for our expense tracker. We have a way to store, add, and retrieve expense data. The use of classes and interfaces keeps the code organized and type-safe.

Building the User Interface (UI) with HTML and CSS

Now, let’s create a simple UI for our expense tracker using HTML and CSS. Create an `index.html` file in the root of your project:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Expense Tracker</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>Expense Tracker</h1>

        <div id="expense-form">
            <label for="description">Description:</label>
            <input type="text" id="description" name="description"><br>

            <label for="amount">Amount:</label>
            <input type="number" id="amount" name="amount"><br>

            <label for="date">Date:</label>
            <input type="date" id="date" name="date"><br>

            <label for="category">Category:</label>
            <select id="category" name="category">
                <!-- Categories will be populated here -->
            </select><br>

            <button id="add-expense-button">Add Expense</button>
        </div>

        <div id="expense-list">
            <h2>Expenses</h2>
            <ul id="expenses">
                <!-- Expenses will be listed here -->
            </ul>
            <p id="total-expenses">Total: $0.00</p>
        </div>
    </div>
    <script src="./dist/index.js"></script>
</body>
</html>

This HTML provides the basic structure for our expense tracker, including:

  • A heading (`<h1>`) for the title.
  • An expense form with input fields for description, amount, date, and category.
  • A category dropdown (`<select>`).
  • An “Add Expense” button.
  • A section to display the expense list (`<ul>`).
  • A display for the total expenses (`<p>`).
  • A link to the `style.css` file for styling.
  • A link to the compiled JavaScript file (`./dist/index.js`).

Now, let’s create a basic CSS file called `style.css` in the root directory to style the HTML:

/* style.css */
body {
    font-family: sans-serif;
    margin: 0;
    padding: 0;
    background-color: #f4f4f4;
}

.container {
    width: 80%;
    margin: 20px auto;
    background-color: #fff;
    padding: 20px;
    border-radius: 5px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

h1 {
    text-align: center;
    color: #333;
}

label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
}

input[type="text"], input[type="number"], input[type="date"], select {
    width: 100%;
    padding: 8px;
    margin-bottom: 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    box-sizing: border-box;
}

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

button:hover {
    background-color: #3e8e41;
}

ul {
    list-style: none;
    padding: 0;
}

li {
    padding: 10px;
    margin-bottom: 5px;
    border: 1px solid #ddd;
    border-radius: 4px;
    background-color: #f9f9f9;
}

#total-expenses {
    font-weight: bold;
    margin-top: 10px;
}

This CSS provides basic styling for the HTML elements, making the expense tracker visually appealing.

Connecting the UI with TypeScript Logic

Now, we’ll connect the HTML UI with our TypeScript logic. We’ll add event listeners to the “Add Expense” button and populate the category dropdown. Modify `src/index.ts` to include the following:

// src/index.ts (modified)
import { Expense, Category } from './types';

class ExpenseTracker {
  private expenses: Expense[] = [];
  private categories: Category[] = [
    { id: 1, name: 'Food' },
    { id: 2, name: 'Transportation' },
    { id: 3, name: 'Housing' },
    { id: 4, name: 'Entertainment' },
    { id: 5, name: 'Utilities' },
  ];
  private nextExpenseId: number = 1;

  addExpense(description: string, amount: number, date: string, categoryId: number): void {
    const category = this.categories.find(c => c.id === categoryId);
    if (!category) {
      console.error('Invalid category ID');
      return;
    }

    const newExpense: Expense = {
      id: this.nextExpenseId++,
      description,
      amount,
      date,
      category: category.name,
    };
    this.expenses.push(newExpense);
    this.renderExpenses(); // Re-render the expense list
    this.updateTotal(); // Update the total
    console.log('Expense added:', newExpense);
  }

  getExpenses(): Expense[] {
    return this.expenses;
  }

  getCategories(): Category[] {
    return this.categories;
  }

  getTotalExpenses(): number {
    return this.expenses.reduce((sum, expense) => sum + expense.amount, 0);
  }

  getExpensesByCategory(categoryId: number): Expense[] {
    const category = this.categories.find(c => c.id === categoryId);
    if (!category) {
      console.error('Invalid category ID');
      return [];
    }
    return this.expenses.filter(expense => expense.category === category.name);
  }

  // Method to render expenses in the UI
  renderExpenses(): void {
    const expenseList = document.getElementById('expenses') as HTMLUListElement;
    if (!expenseList) return;

    expenseList.innerHTML = ''; // Clear existing list
    this.expenses.forEach(expense => {
      const listItem = document.createElement('li');
      listItem.textContent = `${expense.description} - $${expense.amount} - ${expense.date} - ${expense.category}`;
      expenseList.appendChild(listItem);
    });
  }

  // Method to update the total expenses in the UI
  updateTotal(): void {
    const totalExpensesElement = document.getElementById('total-expenses') as HTMLParagraphElement;
    if (!totalExpensesElement) return;
    totalExpensesElement.textContent = `Total: $${this.getTotalExpenses().toFixed(2)}`;
  }

  // Method to populate the category dropdown
  populateCategories(): void {
    const categorySelect = document.getElementById('category') as HTMLSelectElement;
    if (!categorySelect) return;

    this.categories.forEach(category => {
      const option = document.createElement('option');
      option.value = String(category.id);
      option.textContent = category.name;
      categorySelect.appendChild(option);
    });
  }
}

// Initialize the ExpenseTracker
const tracker = new ExpenseTracker();

// Get references to HTML elements
const descriptionInput = document.getElementById('description') as HTMLInputElement;
const amountInput = document.getElementById('amount') as HTMLInputElement;
const dateInput = document.getElementById('date') as HTMLInputElement;
const categorySelect = document.getElementById('category') as HTMLSelectElement;
const addExpenseButton = document.getElementById('add-expense-button') as HTMLButtonElement;

// Populate categories on page load
tracker.populateCategories();

// Add event listener to the "Add Expense" button
addExpenseButton?.addEventListener('click', () => {
  const description = descriptionInput.value;
  const amount = parseFloat(amountInput.value);
  const date = dateInput.value;
  const categoryId = parseInt(categorySelect.value);

  if (description && !isNaN(amount) && date && !isNaN(categoryId)) {
    tracker.addExpense(description, amount, date, categoryId);
    // Clear the form after adding an expense
    descriptionInput.value = '';
    amountInput.value = '';
    dateInput.value = '';
    categorySelect.value = ''; // Reset to default
  } else {
    alert('Please fill in all fields correctly.');
  }
});

Key changes in this updated code:

  • `renderExpenses()` method: This method dynamically renders the expenses in the `expense-list` section of the HTML. It iterates through the `expenses` array and creates `<li>` elements for each expense.
  • `updateTotal()` method: This method updates the total expenses displayed in the UI.
  • `populateCategories()` method: This method populates the category dropdown (`<select>`) with options from the `categories` array.
  • HTML element references: We get references to the HTML elements using `document.getElementById()`. We use type assertions (e.g., `as HTMLInputElement`) to tell TypeScript the expected type of the element.
  • Event listener: We add an event listener to the “Add Expense” button. When clicked, it retrieves the input values, validates them, and calls the `addExpense()` method.
  • Clear Form: The code now clears the form after a successful expense addition, improving the user experience.
  • Validation: Includes basic input validation to ensure data is entered correctly.
  • Initial Population: Calls `populateCategories()` to populate the category dropdown on page load.
  • Re-rendering: Calls `renderExpenses()` and `updateTotal()` after adding an expense to update the UI.

After making these changes, compile the TypeScript code using `npm run build`. Open `index.html` in your browser. You should now be able to add expenses, and they will be displayed in the list. The total expenses will also be updated.

Advanced Features and Enhancements

This is a basic expense tracker. Here are some ideas for advanced features and enhancements:

  • Data Persistence: Store expenses in local storage or a database so the data persists across sessions.
  • Edit and Delete Expenses: Add functionality to edit and delete existing expenses.
  • Filtering and Sorting: Implement filtering and sorting options to view expenses by date, category, or amount.
  • Budgeting: Add budgeting functionality to set monthly or yearly budgets.
  • Reporting and Charts: Generate reports and charts to visualize spending habits.
  • User Authentication: Implement user authentication to allow multiple users to track their expenses.
  • Responsive Design: Make the UI responsive for different screen sizes.
  • Error Handling: Improve error handling and provide user-friendly error messages.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to avoid them:

  • Incorrect TypeScript Setup: Ensure your `tsconfig.json` is configured correctly. Double-check the `outDir`, `rootDir`, and `module` settings. Make sure you have installed TypeScript and the necessary type definitions.
  • Type Errors: TypeScript will help catch type errors during development. Pay close attention to the compiler’s error messages and fix the type mismatches. Use type assertions (`as`) carefully.
  • Incorrect HTML Element References: Make sure your HTML element IDs match the IDs you are using in your JavaScript code. Use `document.getElementById()` correctly and use the correct type assertions.
  • Event Listener Issues: Ensure your event listeners are correctly attached to the HTML elements. Check for typos and ensure the elements exist in the DOM when the JavaScript code runs.
  • Data Validation: Always validate user input to prevent unexpected behavior and errors. Check for empty fields, invalid data types, and other potential issues.
  • CSS Styling Problems: If your CSS isn’t working, check the following:
    • Make sure the `<link>` tag in your HTML correctly references your CSS file.
    • Check for typos in your CSS selectors and property names.
    • Use your browser’s developer tools (usually accessed by pressing F12) to inspect the CSS and see if there are any errors or conflicts.

By carefully reviewing these potential issues, you can minimize debugging time and produce a more robust and functional application.

Key Takeaways

This tutorial provided a step-by-step guide to building a simple web-based expense tracker using TypeScript. We covered the following key concepts:

  • Project setup: Setting up a TypeScript project with npm, installing necessary packages, and configuring the `tsconfig.json` file.
  • Data structures: Defining data structures using TypeScript interfaces.
  • Core logic: Implementing the expense tracker logic with classes and methods.
  • User interface (UI): Creating a basic UI using HTML and CSS.
  • Connecting UI and logic: Connecting the UI to the TypeScript logic using JavaScript and event listeners.
  • Adding advanced features: Discussing potential enhancements like data persistence, filtering, and reporting.
  • Troubleshooting: Identifying and resolving common mistakes.

By completing this project, you’ve gained practical experience with TypeScript, web development fundamentals, and financial tracking. You now have a solid foundation to build more complex web applications.

Frequently Asked Questions (FAQ)

  1. Can I use a different JavaScript framework? Yes, you can. This tutorial focuses on the core concepts of TypeScript and web development. You can adapt the code to use a framework like React, Angular, or Vue.js. The fundamental principles will remain the same.
  2. How can I store the expense data persistently? You can use local storage, a browser-based storage mechanism, to save data between sessions. For more robust storage, consider using a backend database (e.g., MongoDB, PostgreSQL) and an API.
  3. How do I handle dates in different formats? You can use a library like Moment.js or date-fns to format and parse dates. Alternatively, you can use the built-in `Date` object in JavaScript.
  4. How can I deploy this application? You can deploy your application to a hosting platform like Netlify, Vercel, or GitHub Pages. These platforms provide free hosting for static websites. You’ll need to build your TypeScript code (using `npm run build`) and upload the contents of the `dist` directory.

Building this expense tracker has provided a practical application of TypeScript principles and web development techniques. The process of structuring the application, defining data models, and connecting the front-end to the back-end logic mirrors the approach used in more complex projects. The ability to create a functional tool from scratch empowers you to approach future coding challenges with greater confidence. This project serves as a starting point, encouraging further exploration and refinement of your skills, ultimately leading to a deeper understanding of software development and its practical applications. The journey of building the expense tracker is less about the final product and more about the knowledge gained, the problems solved, and the satisfaction of bringing an idea to life through code.