TypeScript Tutorial: Building a Simple Expense Tracker App

Managing finances can be a daunting task. Tracking income and expenses, budgeting, and understanding where your money goes often feels overwhelming. Wouldn’t it be great to have a simple, intuitive tool to help you gain control of your finances? This tutorial will guide you through building a basic expense tracker application using TypeScript, a powerful and versatile language that brings structure and scalability to your JavaScript projects. We’ll cover everything from setting up your development environment to implementing core features like adding transactions, displaying summaries, and handling data.

Why TypeScript?

TypeScript, a superset of JavaScript, adds static typing to your code. This means you can define the types of variables, function parameters, and return values. This offers several advantages:

  • Early Error Detection: TypeScript catches type-related errors during development, before runtime. This significantly reduces the chances of bugs in your application.
  • Improved Code Readability: Types make your code easier to understand and maintain. They act as self-documenting elements, clarifying the purpose of variables and functions.
  • Enhanced Code Completion: IDEs (Integrated Development Environments) can provide better code completion and suggestions, making you more productive.
  • Scalability: TypeScript is excellent for large projects. It helps you manage complex codebases more effectively.

Setting Up Your Development Environment

Before we start coding, let’s set up our development environment. You’ll need the following:

  • Node.js and npm (Node Package Manager): These are essential for running JavaScript and managing project dependencies. You can download them from https://nodejs.org/.
  • A Code Editor: Visual Studio Code (VS Code) is highly recommended due to its excellent TypeScript support and extensions. You can download it from https://code.visualstudio.com/.

Once you have Node.js and npm installed, open your terminal or command prompt and create a new project directory:

mkdir expense-tracker-app
cd expense-tracker-app

Initialize your project with npm:

npm init -y

This command creates a package.json file, which will store your project’s dependencies and metadata.

Next, install TypeScript globally (optional, but convenient):

npm install -g typescript

Now, let’s initialize a TypeScript configuration file (tsconfig.json):

tsc --init

This creates a tsconfig.json file in your project. This file contains various compiler options that control how TypeScript compiles your code. You can customize this file to suit your project’s needs. For now, we’ll keep the default settings.

Creating the Project Structure

Let’s create the basic file structure for our expense tracker app. In your project directory, create the following folders and files:

  • src/ (This folder will contain our TypeScript source code)
    • models/
      • transaction.ts (Defines the Transaction model)
    • index.ts (The main entry point for our application)
  • dist/ (This folder will contain the compiled JavaScript code)
  • index.html (The HTML file for our application)

Defining the Transaction Model

Let’s start by defining a Transaction model. This model will represent a single expense or income entry. Open src/models/transaction.ts and add the following code:

// src/models/transaction.ts

export interface Transaction {
  id: number;
  description: string;
  amount: number;
  type: 'income' | 'expense'; // Use a union type for better type safety
  date: Date;
}

Here, we define an interface called Transaction. This interface specifies the properties of a transaction, including:

  • id: A unique identifier for the transaction (number).
  • description: A brief description of the transaction (string).
  • amount: The amount of the transaction (number).
  • type: The type of transaction, either ‘income’ or ‘expense’. We use a union type ('income' | 'expense') to restrict the possible values.
  • date: The date of the transaction (Date object).

Implementing the Core Logic in index.ts

Now, let’s implement the core logic for our expense tracker. Open src/index.ts and add the following code:

// src/index.ts
import { Transaction } from './models/transaction';

// Sample data (replace with your data source, e.g., local storage)
let transactions: Transaction[] = [
  {
    id: 1,
    description: 'Salary',
    amount: 3000,
    type: 'income',
    date: new Date('2024-01-15'),
  },
  {
    id: 2,
    description: 'Groceries',
    amount: 100,
    type: 'expense',
    date: new Date('2024-01-16'),
  },
  {
    id: 3,
    description: 'Rent',
    amount: 1000,
    type: 'expense',
    date: new Date('2024-01-01'),
  },
];

// Function to add a new transaction
function addTransaction(description: string, amount: number, type: 'income' | 'expense'): void {
  const id = transactions.length + 1;
  const newTransaction: Transaction = {
    id,
    description,
    amount,
    type,
    date: new Date(),
  };
  transactions.push(newTransaction);
  renderTransactions(); // Re-render the transactions list after adding a new transaction
}

// Function to calculate the balance
function calculateBalance(): number {
  let income = 0;
  let expenses = 0;

  transactions.forEach((transaction) => {
    if (transaction.type === 'income') {
      income += transaction.amount;
    } else {
      expenses += transaction.amount;
    }
  });

  return income - expenses;
}

// Function to render transactions in the UI
function renderTransactions(): void {
  const transactionList = document.getElementById('transactionList') as HTMLUListElement | null;
  const balanceElement = document.getElementById('balance') as HTMLElement | null;

  if (!transactionList || !balanceElement) {
    console.error('Missing UI elements');
    return;
  }

  transactionList.innerHTML = ''; // Clear existing transactions

  transactions.forEach((transaction) => {
    const listItem = document.createElement('li');
    listItem.innerHTML = `
      <span>${transaction.description}</span>
      <span>${transaction.amount.toFixed(2)}</span>
      <span>${transaction.type}</span>
      <span>${transaction.date.toLocaleDateString()}</span>
    `;
    transactionList.appendChild(listItem);
  });

  balanceElement.textContent = calculateBalance().toFixed(2);
}

// Initial render
renderTransactions();

// Example: Adding a transaction
addTransaction('Freelance Work', 500, 'income');

Let’s break down this code:

  • Import Statement: import { Transaction } from './models/transaction'; imports the Transaction interface we defined earlier.
  • Sample Data: transactions: Transaction[] = [...] initializes an array of Transaction objects. In a real application, you would likely fetch this data from a database or local storage.
  • addTransaction() Function: This function adds a new transaction to the transactions array. It takes the description, amount, and type as input and creates a new Transaction object. It then calls renderTransactions() to update the UI.
  • calculateBalance() Function: This function calculates the current balance by summing income and expenses.
  • renderTransactions() Function: This function is responsible for updating the user interface (UI) to display the transactions and the current balance. It retrieves the <ul> element with the ID “transactionList” and the element with the ID “balance” from the HTML, clears the existing content, and then iterates through the transactions array, creating <li> elements for each transaction and appending them to the list. It also updates the balance displayed in the UI.
  • Initial Render: renderTransactions(); calls the render function to display the initial data when the application starts.
  • Example Usage: addTransaction('Freelance Work', 500, 'income'); demonstrates how to add a new transaction. This is for testing purposes.

Creating the HTML User Interface (UI)

Now, let’s create a simple HTML file to display our expense tracker. Open index.html 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;
    }
    #transactionList {
      list-style: none;
      padding: 0;
    }
    #transactionList li {
      display: flex;
      justify-content: space-between;
      padding: 5px 0;
      border-bottom: 1px solid #ccc;
    }
  </style>
</head>
<body>
  <h2>Expense Tracker</h2>
  <div>
    <h3>Balance: <span id="balance">0.00</span></h3>
  </div>
  <ul id="transactionList">
    <!-- Transactions will be added here -->
  </ul>
  <script src="./dist/index.js"></script>
</body>
</html>

This HTML file creates a basic layout for our expense tracker:

  • A heading (<h2>) for the title.
  • A div to display the balance, with an <span> element (id="balance") to show the balance value.
  • An unordered list (<ul id="transactionList">) to display the transactions.
  • A <script> tag to include the compiled JavaScript file (dist/index.js).
  • Basic CSS to style the page.

Compiling and Running the Application

Now, let’s compile our TypeScript code into JavaScript and run the application. Open your terminal and run the following command from your project root directory:

tsc

This command will compile all TypeScript files in your src directory and generate corresponding JavaScript files in the dist directory. If there are any type errors, the compiler will report them, and the compilation will fail. This is one of the key benefits of using TypeScript – catching errors early. If the compilation is successful, you should see a index.js file in your dist folder.

To run the application, open index.html in your web browser. You should see the initial data rendered in the UI, including the balance and the list of transactions. You can open the developer console in your browser (usually by pressing F12) to see any console messages.

Adding More Features

Let’s enhance our application with more features to make it more functional:

1. Adding Transactions via Input Fields

Currently, we are adding transactions directly in the code. Let’s add input fields to allow users to enter transaction details. Modify index.html to include input fields for description, amount, and type:

<!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;
    }
    #transactionList {
      list-style: none;
      padding: 0;
    }
    #transactionList li {
      display: flex;
      justify-content: space-between;
      padding: 5px 0;
      border-bottom: 1px solid #ccc;
    }
  </style>
</head>
<body>
  <h2>Expense Tracker</h2>
  <div>
    <h3>Balance: <span id="balance">0.00</span></h3>
  </div>
  <div>
    <label for="description">Description:</label>
    <input type="text" id="description"><br>
    <label for="amount">Amount:</label>
    <input type="number" id="amount"><br>
    <label for="type">Type:</label>
    <select id="type">
      <option value="income">Income</option>
      <option value="expense">Expense</option>
    </select><br>
    <button id="addTransactionButton">Add Transaction</button>
  </div>
  <ul id="transactionList">
    <!-- Transactions will be added here -->
  </ul>
  <script src="./dist/index.js"></script>
</body>
</html>

Next, update src/index.ts to handle the input values and add a transaction on button click:

// src/index.ts
import { Transaction } from './models/transaction';

// ... (rest of the code)

// Function to add a new transaction
function addTransaction(description: string, amount: number, type: 'income' | 'expense'): void {
  if (!description || amount === 0) {
    alert('Please fill in all fields.');
    return;
  }

  const id = transactions.length + 1;
  const newTransaction: Transaction = {
    id,
    description,
    amount,
    type,
    date: new Date(),
  };
  transactions.push(newTransaction);
  renderTransactions();
}

// Add event listener to the button
const addTransactionButton = document.getElementById('addTransactionButton') as HTMLButtonElement | null;

if (addTransactionButton) {
  addTransactionButton.addEventListener('click', () => {
    const descriptionInput = document.getElementById('description') as HTMLInputElement | null;
    const amountInput = document.getElementById('amount') as HTMLInputElement | null;
    const typeSelect = document.getElementById('type') as HTMLSelectElement | null;

    if (!descriptionInput || !amountInput || !typeSelect) {
      console.error('Missing input elements');
      return;
    }

    const description = descriptionInput.value;
    const amount = parseFloat(amountInput.value);
    const type = typeSelect.value as 'income' | 'expense';

    addTransaction(description, amount, type);

    // Clear input fields after adding transaction
    descriptionInput.value = '';
    amountInput.value = '';
  });
}

Key changes:

  • Added input fields and a button in the HTML.
  • Added an event listener to the “Add Transaction” button.
  • Inside the event listener, we retrieve the values from the input fields.
  • We call the addTransaction() function with the input values.
  • After adding the transaction, we clear the input fields.
  • Added basic input validation to prevent empty transactions.

2. Displaying Transaction Dates and Formatting

Currently, the dates are displayed in a default format. Let’s format them for better readability. Modify the renderTransactions() function in src/index.ts:

// src/index.ts
// ... (rest of the code)

// Function to render transactions in the UI
function renderTransactions(): void {
  const transactionList = document.getElementById('transactionList') as HTMLUListElement | null;
  const balanceElement = document.getElementById('balance') as HTMLElement | null;

  if (!transactionList || !balanceElement) {
    console.error('Missing UI elements');
    return;
  }

  transactionList.innerHTML = ''; // Clear existing transactions

  transactions.forEach((transaction) => {
    const listItem = document.createElement('li');
    listItem.innerHTML = `
      <span>${transaction.description}</span>
      <span>${transaction.amount.toFixed(2)}</span>
      <span>${transaction.type}</span>
      <span>${transaction.date.toLocaleDateString()}</span>
    `;
    transactionList.appendChild(listItem);
  });

  balanceElement.textContent = calculateBalance().toFixed(2);
}

We’re using toLocaleDateString() to format the date. This method formats the date according to the user’s locale settings.

3. Adding Visual Cues for Income and Expenses

Let’s add some visual cues to differentiate between income and expenses. We can change the text color based on the transaction type. Modify renderTransactions() function in src/index.ts:

// src/index.ts
// ... (rest of the code)

// Function to render transactions in the UI
function renderTransactions(): void {
  const transactionList = document.getElementById('transactionList') as HTMLUListElement | null;
  const balanceElement = document.getElementById('balance') as HTMLElement | null;

  if (!transactionList || !balanceElement) {
    console.error('Missing UI elements');
    return;
  }

  transactionList.innerHTML = ''; // Clear existing transactions

  transactions.forEach((transaction) => {
    const listItem = document.createElement('li');
    listItem.innerHTML = `
      <span>${transaction.description}</span>
      <span style="color: ${transaction.type === 'expense' ? 'red' : 'green'}">${transaction.amount.toFixed(2)}</span>
      <span>${transaction.type}</span>
      <span>${transaction.date.toLocaleDateString()}</span>
    `;
    transactionList.appendChild(listItem);
  });

  balanceElement.textContent = calculateBalance().toFixed(2);
}

We added inline styles to the amount span to change the color based on the transaction type.

Common Mistakes and How to Fix Them

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

  • Ignoring Type Errors: TypeScript’s main advantage is its type checking. Don’t ignore the errors the compiler reports. They are there to help you catch bugs early. Read the error messages carefully and understand what’s causing the issue.
  • Not Using Interfaces/Types: Interfaces and types are crucial for defining the structure of your data. Failing to use them leads to less maintainable and more error-prone code. Use interfaces to define the shape of your objects.
  • Incorrectly Typing Variables: Make sure you are using the correct types for your variables. For example, if a variable should hold a number, declare it as number, not string.
  • Not Understanding Compiler Options: The tsconfig.json file contains various compiler options. Take the time to understand these options to configure your project correctly. For example, setting "strict": true in your tsconfig.json file enables stricter type checking.
  • Mixing JavaScript and TypeScript: While TypeScript can work with JavaScript, avoid mixing the two unnecessarily. Write your code primarily in TypeScript to get the full benefits of the language.
  • Not Using a Code Editor with TypeScript Support: Using a code editor with good TypeScript support (like VS Code) is essential. It provides features like code completion, error highlighting, and refactoring, which significantly improve your productivity.
  • Forgetting to Compile: Always compile your TypeScript code before running it. Make sure you run tsc to generate the JavaScript files.

Key Takeaways and Summary

In this tutorial, we’ve built a simple expense tracker application using TypeScript. We’ve covered the following key concepts:

  • Setting up a TypeScript development environment.
  • Defining data models using interfaces.
  • Implementing core application logic.
  • Creating a basic HTML UI.
  • Handling user input.
  • Compiling and running TypeScript code.
  • Adding features to enhance the application.

This tutorial provides a solid foundation for building more complex applications with TypeScript. You can extend this application by adding features like:

  • Data persistence (e.g., using local storage or a database).
  • More advanced UI elements (e.g., charts and graphs for data visualization).
  • User authentication.
  • Categorization of expenses and income.
  • Budgeting features.

FAQ

  1. Why use TypeScript instead of JavaScript? TypeScript adds static typing, which helps catch errors during development, improves code readability, and makes your code more maintainable, especially for larger projects.
  2. How do I install TypeScript? You can install TypeScript globally using npm: npm install -g typescript.
  3. What is a tsconfig.json file? It’s a configuration file that tells the TypeScript compiler how to compile your code.
  4. How do I compile TypeScript code? Use the command tsc in your terminal.
  5. Where can I learn more about TypeScript? The official TypeScript documentation is an excellent resource: https://www.typescriptlang.org/docs/.

This is just the beginning. The world of TypeScript is vast, and there’s always more to learn. Keep exploring, experimenting, and building. The skills you’ve gained here will serve as a solid foundation for your future TypeScript projects. Remember to practice regularly, read other people’s code, and don’t be afraid to experiment. Happy coding!