TypeScript Tutorial: Creating a Simple Web Application for a Basic Expense Tracker

Managing finances can be a daunting task. Keeping track of income, expenses, and overall budget can feel overwhelming without the right tools. Spreadsheets are often the go-to solution, but they can be clunky and lack the visual appeal and real-time feedback that makes financial management easier. This tutorial will guide you through building a simple, yet effective, expense tracker web application using TypeScript. We’ll explore fundamental TypeScript concepts while creating a practical application that you can customize and expand upon to meet your personal finance needs. This project is designed for developers with some experience in JavaScript, ready to dive into TypeScript and build something useful.

Why Build an Expense Tracker?

An expense tracker provides a clear view of your spending habits. It helps you:

  • Identify Spending Patterns: See where your money is going.
  • Budget Effectively: Set and track budgets for different categories.
  • Achieve Financial Goals: Make informed decisions to save and invest.
  • Gain Financial Awareness: Understand your cash flow.

Building your own expense tracker offers several advantages over using pre-built software. You have complete control over features, data privacy, and the user interface. It also serves as a great learning experience in TypeScript and web development.

Prerequisites

Before we begin, ensure you have the following installed:

  • Node.js and npm (or yarn): Used for package management and running the application.
  • A Code Editor: Visual Studio Code (VS Code) is recommended, but you can use any editor you prefer.
  • Basic Understanding of HTML, CSS, and JavaScript: Familiarity with these technologies is essential for building web applications.

Setting Up the Project

Let’s start by setting up the project. Open your terminal and navigate to your desired project directory. Then, run the following commands:

mkdir expense-tracker
cd expense-tracker
npm init -y
npm install typescript --save-dev
npm install --save-dev @types/node

These commands create a new project directory, initialize a `package.json` file, install TypeScript as a development dependency, and install type definitions for Node.js.

Creating the `tsconfig.json` File

Next, create a `tsconfig.json` file in your project root. This file configures the TypeScript compiler. You can generate a basic `tsconfig.json` file using the following command:

npx tsc --init

This command creates a `tsconfig.json` file with a set of default compiler options. You can customize these options to fit your project’s needs. For this tutorial, we will use the following configuration:

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

Here’s what each option means:

  • target: Specifies the JavaScript version to compile to (ES5 is widely supported).
  • module: Defines the module system (CommonJS is suitable for Node.js).
  • outDir: Sets the output directory for compiled JavaScript files.
  • rootDir: Specifies the root directory of your TypeScript source files.
  • strict: Enables strict type checking.
  • esModuleInterop: Enables interoperability between CommonJS and ES modules.
  • skipLibCheck: Skips type checking of declaration files.
  • forceConsistentCasingInFileNames: Enforces consistent casing in file names.
  • include: Specifies the files to include in the compilation.

Project Structure

Create the following directory structure in your project:

expense-tracker/
├── src/
│   ├── index.ts
│   └── models/
│       └── expense.ts
├── dist/
├── node_modules/
├── package.json
├── tsconfig.json
└── ...

The src directory will contain your TypeScript source files, and the dist directory will hold the compiled JavaScript files.

Defining Data Models

Let’s define the data structures for our expense tracker. We’ll start with an Expense model. Create a file named expense.ts inside the src/models directory and add the following code:

// src/models/expense.ts

export interface Expense {
  id: number;
  description: string;
  amount: number;
  category: string;
  date: Date;
}

This interface defines the properties of an expense: id (unique identifier), description, amount, category, and date. Using interfaces allows us to define the shape of our data and ensure type safety throughout the application.

Building the Core Logic

Now, let’s write the core logic for managing expenses. Open src/index.ts and add the following code:

// src/index.ts
import { Expense } from './models/expense';

let expenses: Expense[] = [];

function addExpense(expense: Expense): void {
  expense.id = expenses.length + 1; // Basic ID generation
  expenses.push(expense);
  console.log('Expense added:', expense);
}

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

function calculateTotalExpenses(): number {
  return expenses.reduce((total, expense) => total + expense.amount, 0);
}

// Example usage
const newExpense: Expense = {
  description: 'Groceries',
  amount: 50,
  category: 'Food',
  date: new Date(),
  id: 0 // ID will be generated by addExpense
};

addExpense(newExpense);

const anotherExpense: Expense = {
  description: 'Coffee',
  amount: 5,
  category: 'Food',
  date: new Date(),
  id: 0 // ID will be generated by addExpense
};

addExpense(anotherExpense);

console.log('All Expenses:', getExpenses());
console.log('Total Expenses:', calculateTotalExpenses());

Here’s a breakdown of the code:

  • Import Expense Interface: Imports the Expense interface from ./models/expense.
  • Expenses Array: Initializes an empty array expenses to store expense objects.
  • addExpense Function: Adds a new expense to the expenses array. It also assigns a basic ID to the expense (you would likely use a more robust ID generation strategy in a real application).
  • getExpenses Function: Returns all expenses.
  • calculateTotalExpenses Function: Calculates the total amount of all expenses using the reduce method.
  • Example Usage: Creates two example expense objects and adds them to the expenses array. It then logs the expenses and the total to the console.

Compiling and Running the Application

To compile the TypeScript code, run the following command in your terminal:

npx tsc

This command compiles all TypeScript files in the src directory and outputs the JavaScript files to the dist directory.

To run the application, use Node.js:

node dist/index.js

You should see the expense details and the total expenses printed in your console.

Enhancing the Application with a Simple UI

Let’s create a basic HTML file to display the expenses. 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>Expense Tracker</title>
    <style>
        body {
            font-family: sans-serif;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 20px;
        }
        th, td {
            border: 1px solid #ddd;
            padding: 8px;
            text-align: left;
        }
        th {
            background-color: #f2f2f2;
        }
    </style>
</head>
<body>
    <h2>Expense Tracker</h2>
    <table id="expenseTable">
        <thead>
            <tr>
                <th>ID</th>
                <th>Description</th>
                <th>Amount</th>
                <th>Category</th>
                <th>Date</th>
            </tr>
        </thead>
        <tbody id="expenseTableBody">
            <!-- Expenses will be displayed here -->
        </tbody>
    </table>
    <p>Total Expenses: <span id="totalExpenses">0</span></p>
    <script src="dist/index.js"></script>
</body>
</html>

This HTML file sets up a basic table to display expenses and a paragraph to show the total. It also includes a link to the compiled JavaScript file (dist/index.js). We will modify the index.ts file to update the HTML.

Modify the src/index.ts file to update the HTML with expense data:

// src/index.ts
import { Expense } from './models/expense';

let expenses: Expense[] = [];

function addExpense(expense: Expense): void {
  expense.id = expenses.length + 1; // Basic ID generation
  expenses.push(expense);
  console.log('Expense added:', expense);
  renderExpenses(); // Update the UI after adding an expense
}

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

function calculateTotalExpenses(): number {
  return expenses.reduce((total, expense) => total + expense.amount, 0);
}

function renderExpenses(): void {
  const tableBody = document.getElementById('expenseTableBody') as HTMLTableSectionElement | null;
  const totalExpensesElement = document.getElementById('totalExpenses') as HTMLSpanElement | null;

  if (!tableBody || !totalExpensesElement) {
    console.error('Could not find table body or total expenses element.');
    return;
  }

  // Clear existing rows
  tableBody.innerHTML = '';

  // Populate the table with expenses
  getExpenses().forEach(expense => {
    const row = document.createElement('tr');
    row.innerHTML = `
      <td>${expense.id}</td>
      <td>${expense.description}</td>
      <td>$${expense.amount.toFixed(2)}</td>
      <td>${expense.category}</td>
      <td>${expense.date.toLocaleDateString()}</td>
    `;
    tableBody.appendChild(row);
  });

  // Update the total
  totalExpensesElement.textContent = calculateTotalExpenses().toFixed(2);
}

// Example usage
const newExpense: Expense = {
  description: 'Groceries',
  amount: 50,
  category: 'Food',
  date: new Date(),
  id: 0 // ID will be generated by addExpense
};

addExpense(newExpense);

const anotherExpense: Expense = {
  description: 'Coffee',
  amount: 5,
  category: 'Food',
  date: new Date(),
  id: 0 // ID will be generated by addExpense
};

addExpense(anotherExpense);

// Initial render
renderExpenses();

Here’s what changed:

  • renderExpenses Function: This function is responsible for updating the HTML table with expense data. It retrieves the table body and total expenses elements, clears the existing rows, and then iterates through the expenses array, creating a new row for each expense. It also updates the total expenses displayed on the page.
  • HTML Element Selection: The code now uses document.getElementById to select the table body and total expenses element. The use of the `as HTMLTableSectionElement | null` and `as HTMLSpanElement | null` assertions is important because `document.getElementById` returns a generic `HTMLElement` type, and we need to tell TypeScript what type of HTML element it is.
  • UI Update Trigger: The renderExpenses() function is called after adding an expense and at the end of the file for the initial display.

Now, compile the TypeScript code again (npx tsc) and open index.html in your browser. You should see the expenses displayed in the table, along with the total. Note that you will need to serve the HTML file from a local web server to access the JavaScript file. You can use a simple server like `http-server` (install it globally with `npm install -g http-server`) or use a VS Code extension like Live Server.

Adding Input Forms

To make the expense tracker more interactive, let’s add an HTML form for users to input new expenses. Add the following HTML code inside the <body> tags of your index.html file, above the table:

<h3>Add Expense</h3>
<form id="expenseForm">
    <label for="description">Description:</label>
    <input type="text" id="description" name="description" required><br>

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

    <label for="category">Category:</label>
    <input type="text" id="category" name="category" required><br>

    <button type="submit">Add Expense</button>
</form>

Now, let’s add the functionality to handle form submissions in src/index.ts. Modify the src/index.ts file as follows:

// src/index.ts
import { Expense } from './models/expense';

let expenses: Expense[] = [];

function addExpense(expense: Expense): void {
  expense.id = expenses.length + 1; // Basic ID generation
  expenses.push(expense);
  console.log('Expense added:', expense);
  renderExpenses(); // Update the UI after adding an expense
}

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

function calculateTotalExpenses(): number {
  return expenses.reduce((total, expense) => total + expense.amount, 0);
}

function renderExpenses(): void {
  const tableBody = document.getElementById('expenseTableBody') as HTMLTableSectionElement | null;
  const totalExpensesElement = document.getElementById('totalExpenses') as HTMLSpanElement | null;

  if (!tableBody || !totalExpensesElement) {
    console.error('Could not find table body or total expenses element.');
    return;
  }

  // Clear existing rows
  tableBody.innerHTML = '';

  // Populate the table with expenses
  getExpenses().forEach(expense => {
    const row = document.createElement('tr');
    row.innerHTML = `
      <td>${expense.id}</td>
      <td>${expense.description}</td>
      <td>$${expense.amount.toFixed(2)}</td>
      <td>${expense.category}</td>
      <td>${expense.date.toLocaleDateString()}</td>
    `;
    tableBody.appendChild(row);
  });

  // Update the total
  totalExpensesElement.textContent = calculateTotalExpenses().toFixed(2);
}

// Add event listener for form submission
document.addEventListener('DOMContentLoaded', () => {
  const expenseForm = document.getElementById('expenseForm') as HTMLFormElement | null;

  if (!expenseForm) {
    console.error('Expense form not found.');
    return;
  }

  expenseForm.addEventListener('submit', (event: Event) => {
    event.preventDefault(); // Prevent the default form submission

    const descriptionInput = document.getElementById('description') as HTMLInputElement | null;
    const amountInput = document.getElementById('amount') as HTMLInputElement | null;
    const categoryInput = document.getElementById('category') as HTMLInputElement | null;

    if (!descriptionInput || !amountInput || !categoryInput) {
      console.error('Form inputs not found.');
      return;
    }

    const description = descriptionInput.value;
    const amount = parseFloat(amountInput.value);
    const category = categoryInput.value;

    if (isNaN(amount)) {
      alert('Please enter a valid amount.');
      return;
    }

    const newExpense: Expense = {
      id: 0, // ID will be generated by addExpense
      description: description,
      amount: amount,
      category: category,
      date: new Date()
    };

    addExpense(newExpense);

    // Clear the form
    descriptionInput.value = '';
    amountInput.value = '';
    categoryInput.value = '';
  });
});

// Example usage
const newExpense: Expense = {
  description: 'Groceries',
  amount: 50,
  category: 'Food',
  date: new Date(),
  id: 0 // ID will be generated by addExpense
};

addExpense(newExpense);

const anotherExpense: Expense = {
  description: 'Coffee',
  amount: 5,
  category: 'Food',
  date: new Date(),
  id: 0 // ID will be generated by addExpense
};

addExpense(anotherExpense);

// Initial render
renderExpenses();

Here’s what changed in the src/index.ts file:

  • Event Listener: Added a DOMContentLoaded event listener to ensure that the JavaScript code runs after the HTML is fully loaded.
  • Form Element Selection: The code now gets a reference to the form element using document.getElementById('expenseForm').
  • Form Submission Handling: An event listener is attached to the form’s submit event. Inside the event listener:
    • event.preventDefault() prevents the default form submission behavior (which would refresh the page).
    • The code retrieves the values from the input fields.
    • Input validation is performed to ensure the amount is a valid number.
    • A new Expense object is created using the form data.
    • The addExpense function is called to add the new expense.
    • The form fields are cleared after submission.

Compile the TypeScript code (npx tsc) and reload your index.html in the browser. You should now be able to add new expenses through the form, and they will appear in the table.

Adding More Features (Optional)

Here are some ideas for extending the functionality of your expense tracker:

  • Expense Categories: Implement a dropdown or a set of predefined categories. This would improve data organization and make it easier to analyze spending.
  • Data Persistence: Save expense data to local storage or a database so that the data persists across sessions.
  • Filtering and Sorting: Add features to filter expenses by date range, category, or amount, and to sort expenses by different criteria.
  • Charts and Visualizations: Use a charting library (like Chart.js) to visualize expense data, such as a pie chart showing spending by category.
  • User Authentication: If you are planning to deploy the application, you might want to add user authentication to secure the user’s data.
  • Error Handling: Implement more robust error handling to provide a better user experience.

Common Mistakes and How to Fix Them

Here are some common mistakes beginners make when working with TypeScript and web applications, along with how to avoid them:

  • Ignoring Type Errors: TypeScript’s primary benefit is type checking. Don’t ignore the type errors that the compiler reports. They are there to help you catch bugs early. Fixing these errors will save you time and headaches later.
  • Incorrect Type Annotations: Make sure you are using the correct type annotations for your variables, function parameters, and return values. If you are unsure of a type, use `any` temporarily, but try to refine the type later.
  • Forgetting to Compile: Always compile your TypeScript code before running your application. The browser only understands JavaScript, so you need to transpile the TypeScript to JavaScript.
  • Incorrectly Using HTML Elements: Be careful when using `document.getElementById()`. Make sure you cast the result to the correct HTML element type (e.g., `HTMLInputElement`, `HTMLTableSectionElement`).
  • Not Handling Null/Undefined: Be aware that HTML element lookups can return `null` if the element isn’t found. Use optional chaining (`?.`) and nullish coalescing (`??`) to handle these cases gracefully.
  • Ignoring the Console: The browser’s developer console is your friend. Use `console.log()` for debugging and to see what’s happening in your code. Pay attention to error messages in the console.

Summary/Key Takeaways

In this tutorial, you learned how to build a basic expense tracker web application using TypeScript. You covered fundamental concepts such as data modeling with interfaces, adding data, displaying data in a simple user interface, and handling user input. You also learned how to compile and run TypeScript code and how to use the developer console for debugging.

By building this application, you have gained practical experience with TypeScript and its application in a real-world scenario. You can now adapt this project to your specific needs, add more features, and expand your understanding of TypeScript and web development. Remember to practice regularly, experiment with different features, and embrace the power of TypeScript’s type system to write more robust and maintainable code.

FAQ

Q: What is TypeScript?

A: TypeScript is a superset of JavaScript that adds static typing. It helps catch errors early in development, improves code readability, and makes it easier to maintain large codebases.

Q: Why should I use TypeScript?

A: TypeScript provides several benefits, including:

  • Type safety
  • Improved code readability and maintainability
  • Better tooling and IDE support
  • Early error detection

Q: How do I compile TypeScript code?

A: You compile TypeScript code using the TypeScript compiler (tsc). You typically run tsc in your terminal, and it will transpile your TypeScript files into JavaScript files.

Q: How can I debug TypeScript code?

A: You can debug TypeScript code using your browser’s developer tools and the console.log() function. You can also use a debugger within your code editor (like VS Code).

Q: Where can I learn more about TypeScript?

A: You can find comprehensive documentation and tutorials on the official TypeScript website: https://www.typescriptlang.org/.

As you continue your journey in web development, remember that the most effective way to learn is by doing. Don’t be afraid to experiment, make mistakes, and learn from them. The concepts and techniques you learned in this tutorial are foundational, and they will serve you well as you tackle more complex projects. Embrace the power of TypeScript and the joy of building applications that bring your ideas to life.