TypeScript Tutorial: Building a Simple Web Application for a Personal Budget Tracker

Managing finances can feel overwhelming. Keeping track of income, expenses, and savings often involves spreadsheets, notebooks, and a lot of manual data entry. What if you could build a simple, yet effective, web application to streamline this process? This tutorial will guide you through creating a personal budget tracker using TypeScript, a powerful superset of JavaScript that adds static typing. We’ll break down the concepts step-by-step, ensuring you understand the ‘why’ behind each line of code, and providing you with a solid foundation for more complex web development projects.

Why TypeScript?

TypeScript offers several advantages over plain JavaScript, especially for larger projects. These benefits include:

  • Improved Code Quality: Static typing helps catch errors early in the development process, reducing the likelihood of runtime bugs.
  • Enhanced Readability: Type annotations make your code easier to understand and maintain, especially when working in teams.
  • Better Tooling: TypeScript provides excellent support for code completion, refactoring, and other IDE features.
  • Scalability: TypeScript makes it easier to scale your application as it grows, thanks to its strong typing and modularity.

Project Setup

Before we dive into the code, let’s set up our project environment. We’ll use Node.js and npm (Node Package Manager) to manage our dependencies. If you don’t have these installed, you can download them from the official Node.js website.

  1. Create a Project Directory: Create a new directory for your project and navigate into it using your terminal:
mkdir budget-tracker
cd budget-tracker
  1. Initialize npm: Initialize a new npm project by running the following command:
npm init -y
  1. Install TypeScript: Install TypeScript globally or locally. For this project, we’ll install it locally as a development dependency:
npm install --save-dev typescript
  1. Create a TypeScript Configuration File: Create a `tsconfig.json` file in your project root. This file tells the TypeScript compiler how to compile your code. You can generate a default configuration by running:
npx tsc --init

This will generate a `tsconfig.json` file with many options. We’ll focus on a few key settings:

{
  "compilerOptions": {
    "target": "es5",  // or "es6", "es2015", etc.
    "module": "commonjs", // or "esnext", "amd", etc.
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}
  • `target`: Specifies the JavaScript version to compile to. `es5` is a good choice for broad browser compatibility.
  • `module`: Specifies the module system to use. `commonjs` is suitable for Node.js environments.
  • `outDir`: Specifies the output directory for the compiled JavaScript files.
  • `rootDir`: Specifies the root directory of your TypeScript source files.
  • `strict`: Enables strict type-checking options. It’s generally a good idea to keep this enabled.
  • `esModuleInterop`: Enables interoperability between CommonJS and ES modules.
  • `skipLibCheck`: Skips type checking of declaration files.
  • `forceConsistentCasingInFileNames`: Enforces consistent casing in filenames.
  • `include`: Specifies which files to include in the compilation.
  1. Create Source Directory: Create a `src` directory to hold your TypeScript source files.
mkdir src

Building the Budget Tracker Application

Now, let’s start building the core functionality of our budget tracker. We’ll start with the data structures and then build the user interface.

1. Defining Data Structures

We’ll need to define the data structures for our budget tracker. We’ll use TypeScript’s `interface` to define the shape of our data.

Create a file named `src/types.ts` and add the following code:

// src/types.ts

export interface Transaction {
  id: number;
  description: string;
  amount: number;
  date: string; // YYYY-MM-DD format
  type: 'income' | 'expense';
}

export interface Budget {
  income: number;
  expenses: number;
  balance: number;
  transactions: Transaction[];
}

Let’s break down these interfaces:

  • `Transaction`: Represents a single transaction. It has the following properties:
    • `id`: A unique identifier for the transaction (number).
    • `description`: A brief description of the transaction (string).
    • `amount`: The monetary value of the transaction (number).
    • `date`: The date of the transaction (string, in YYYY-MM-DD format).
    • `type`: The type of transaction, either ‘income’ or ‘expense’.
  • `Budget`: Represents the overall budget. It has the following properties:
    • `income`: Total income (number).
    • `expenses`: Total expenses (number).
    • `balance`: The current balance (number).
    • `transactions`: An array of `Transaction` objects.

2. Implementing the Budget Logic

Create a file named `src/budget.ts` and add the following code:

// src/budget.ts
import { Transaction, Budget } from './types';

export class BudgetTracker {
  private budget: Budget;

  constructor() {
    this.budget = {
      income: 0,
      expenses: 0,
      balance: 0,
      transactions: [],
    };
  }

  addTransaction(transaction: Transaction): void {
    this.budget.transactions.push(transaction);
    this.updateBudget();
  }

  deleteTransaction(id: number): void {
    this.budget.transactions = this.budget.transactions.filter(transaction => transaction.id !== id);
    this.updateBudget();
  }

  getBudget(): Budget {
    return { ...this.budget }; // Return a copy to prevent direct modification
  }

  private updateBudget(): void {
    this.budget.income = this.budget.transactions
      .filter(transaction => transaction.type === 'income')
      .reduce((sum, transaction) => sum + transaction.amount, 0);

    this.budget.expenses = this.budget.transactions
      .filter(transaction => transaction.type === 'expense')
      .reduce((sum, transaction) => sum + transaction.amount, 0);

    this.budget.balance = this.budget.income - this.budget.expenses;
  }
}

Let’s break down this code:

  • `BudgetTracker` class: This class encapsulates all the budget-related logic.
  • `private budget: Budget;`: A private property to store the current budget data. The `private` keyword ensures that this property can only be accessed from within the class.
  • `constructor()`: Initializes the `budget` with default values (income, expenses, balance as 0 and an empty transactions array).
  • `addTransaction(transaction: Transaction): void`: Adds a new transaction to the `transactions` array and calls `updateBudget()` to recalculate the budget.
  • `deleteTransaction(id: number): void`: Removes a transaction from the `transactions` array based on its ID and calls `updateBudget()`.
  • `getBudget(): Budget`: Returns a copy of the `budget` object. This prevents external code from directly modifying the internal budget data, ensuring data integrity. The spread operator (`…`) is used to create a shallow copy.
  • `private updateBudget(): void`: Recalculates the income, expenses, and balance based on the current transactions. The `private` keyword restricts access to this method within the class.
    • It filters the transactions array to get income and expense transactions.
    • It uses the `reduce` method to calculate the sum of the amounts for each type.
    • It calculates the balance by subtracting expenses from income.

3. Building the User Interface (UI)

For simplicity, we’ll build a basic UI using HTML and JavaScript. We’ll use TypeScript to write the logic that interacts with the UI.

Create an `index.html` file in your project root:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Budget Tracker</title>
    <style>
        body {
            font-family: sans-serif;
            margin: 20px;
        }
        .container {
            max-width: 800px;
            margin: 0 auto;
        }
        .budget-summary {
            display: flex;
            justify-content: space-between;
            margin-bottom: 20px;
            padding: 10px;
            border: 1px solid #ccc;
        }
        .income {
            color: green;
        }
        .expense {
            color: red;
        }
        .balance {
            font-weight: bold;
        }
        .transaction-form {
            margin-bottom: 20px;
        }
        .transaction-list {
            list-style: none;
            padding: 0;
        }
        .transaction-item {
            display: flex;
            justify-content: space-between;
            padding: 10px;
            border-bottom: 1px solid #eee;
        }
        .income-item {
            color: green;
        }
        .expense-item {
            color: red;
        }
        .delete-button {
            background-color: #f44336;
            color: white;
            border: none;
            padding: 5px 10px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>Budget Tracker</h2>

        <div class="budget-summary">
            <div>Income: <span class="income" id="income">$0.00</span></div>
            <div>Expenses: <span class="expense" id="expenses">$0.00</span></div>
            <div class="balance">Balance: <span id="balance">$0.00</span></div>
        </div>

        <div class="transaction-form">
            <h3>Add Transaction</h3>
            <label for="description">Description:</label>
            <input type="text" id="description"><br>
            <label for="amount">Amount:</label>
            <input type="number" id="amount"><br>
            <label for="date">Date:</label>
            <input type="date" id="date"><br>
            <label for="type">Type:</label>
            <select id="type">
                <option value="income">Income</option>
                <option value="expense">Expense</option>
            </select><br>
            <button id="addTransaction">Add Transaction</button>
        </div>

        <h3>Transactions</h3>
        <ul class="transaction-list" id="transactionList">
            <!-- Transactions will be added here -->
        </ul>
    </div>
    <script src="./dist/index.js"></script>
</body>
</html>

This HTML provides the basic structure of our application, including:

  • A budget summary section to display income, expenses, and balance.
  • A form to add new transactions.
  • A list to display existing transactions.

Now, create `src/index.ts` and add the following code:

// src/index.ts
import { BudgetTracker } from './budget';
import { Transaction } from './types';

const budgetTracker = new BudgetTracker();

// DOM elements
const incomeEl = document.getElementById('income') as HTMLSpanElement;
const expensesEl = document.getElementById('expenses') as HTMLSpanElement;
const balanceEl = document.getElementById('balance') as HTMLSpanElement;
const transactionListEl = document.getElementById('transactionList') as HTMLUListElement;
const descriptionInput = document.getElementById('description') as HTMLInputElement;
const amountInput = document.getElementById('amount') as HTMLInputElement;
const dateInput = document.getElementById('date') as HTMLInputElement;
const typeSelect = document.getElementById('type') as HTMLSelectElement;
const addTransactionBtn = document.getElementById('addTransaction') as HTMLButtonElement;

function renderBudget(): void {
  const budget = budgetTracker.getBudget();
  incomeEl.textContent = `$${budget.income.toFixed(2)}`;
  expensesEl.textContent = `$${budget.expenses.toFixed(2)}`;
  balanceEl.textContent = `$${budget.balance.toFixed(2)}`;
}

function renderTransactions(): void {
  transactionListEl.innerHTML = ''; // Clear existing transactions
  const budget = budgetTracker.getBudget();
  budget.transactions.forEach(transaction => {
    const listItem = document.createElement('li');
    listItem.classList.add('transaction-item');
    listItem.classList.add(transaction.type === 'income' ? 'income-item' : 'expense-item');
    listItem.innerHTML = `
      <span>${transaction.description} - $${transaction.amount.toFixed(2)} - ${transaction.date}</span>
      <button class="delete-button" data-id="${transaction.id}">Delete</button>
    `;
    const deleteButton = listItem.querySelector('.delete-button') as HTMLButtonElement;
    if (deleteButton) {
      deleteButton.addEventListener('click', () => {
        budgetTracker.deleteTransaction(transaction.id);
        renderTransactions();
        renderBudget();
      });
    }
    transactionListEl.appendChild(listItem);
  });
}

function addTransactionHandler(): void {
  const description = descriptionInput.value;
  const amount = parseFloat(amountInput.value);
  const date = dateInput.value;
  const type = typeSelect.value as 'income' | 'expense';

  if (!description || isNaN(amount) || !date) {
    alert('Please fill in all fields.');
    return;
  }

  const newTransaction: Transaction = {
    id: Date.now(), // Simple ID generation
    description,
    amount,
    date,
    type,
  };

  budgetTracker.addTransaction(newTransaction);
  renderTransactions();
  renderBudget();

  // Clear input fields
  descriptionInput.value = '';
  amountInput.value = '';
  dateInput.value = '';
  typeSelect.value = 'income';
}

addTransactionBtn.addEventListener('click', addTransactionHandler);

// Initial render
renderBudget();
renderTransactions();

Let’s break down this code:

  • Imports: Imports `BudgetTracker` from `budget.ts` and `Transaction` from `types.ts`.
  • `budgetTracker`: Creates a new instance of the `BudgetTracker` class.
  • DOM Element Selection: Selects various HTML elements by their IDs. Using type assertions (`as HTMLSpanElement`, etc.) to tell TypeScript the type of each element.
  • `renderBudget()`: Updates the budget summary in the UI.
  • `renderTransactions()`: Renders the list of transactions in the UI.
  • `addTransactionHandler()`: Handles the addition of a new transaction.
    • It retrieves the values from the input fields.
    • It validates the input data to ensure all fields are filled.
    • It creates a new `Transaction` object.
    • It calls `budgetTracker.addTransaction()` to add the transaction.
    • It calls `renderTransactions()` and `renderBudget()` to update the UI.
    • It clears the input fields after adding the transaction.
  • Event Listener: Adds an event listener to the “Add Transaction” button to call `addTransactionHandler()` when clicked.
  • Initial Render: Calls `renderBudget()` and `renderTransactions()` to display the initial budget and transactions when the page loads.

4. Compiling and Running the Application

Now that we’ve written the code, let’s compile it and run the application.

  1. Compile TypeScript: Open your terminal in the project directory and run the following command to compile your TypeScript code into JavaScript:
npx tsc

This command will use the configuration in `tsconfig.json` to compile all `.ts` files in the `src` directory into `.js` files in the `dist` directory.

  1. Run the Application: Open `index.html` in your web browser. You should see the basic UI of the budget tracker. You can now add transactions, and the budget summary and transaction list will update accordingly.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to avoid them:

  • Incorrect TypeScript Configuration: Make sure your `tsconfig.json` is set up correctly. Pay close attention to the `outDir`, `rootDir`, and `module` settings. Incorrect settings can lead to compilation errors or unexpected behavior.
  • Type Errors: TypeScript will highlight type errors during development. Read the error messages carefully, and make sure your code adheres to the defined types. Use type annotations and interfaces to help catch these errors early.
  • DOM Element Selection Errors: Make sure your HTML element IDs match the ones in your TypeScript code. Use type assertions (e.g., `as HTMLInputElement`) to help TypeScript understand the type of the selected elements. If an element doesn’t exist, your code may throw an error.
  • Uninitialized Variables: Always initialize your variables before using them. TypeScript can help you catch uninitialized variable errors.
  • Incorrect Event Handling: Ensure your event listeners are correctly attached to the appropriate elements. Double-check the event types (e.g., ‘click’, ‘input’) and the function you are calling.
  • Not Using Version Control: Use Git and a platform like GitHub or GitLab to track your code changes and collaborate with others. This allows you to revert to previous versions if you make mistakes.

Key Takeaways

  • TypeScript Fundamentals: You’ve learned how to use interfaces, classes, and types to structure your code.
  • Project Setup: You’ve learned how to set up a TypeScript project with npm and configure `tsconfig.json`.
  • UI Interaction: You’ve learned how to interact with HTML elements using JavaScript and TypeScript.
  • Budget Tracking Logic: You’ve built the core logic for managing income, expenses, and balance.

FAQ

  1. Can I add more features? Yes! You can extend this application by adding features like:
    • Data persistence (saving data to local storage or a database).
    • More advanced reporting (charts, graphs).
    • User authentication.
  2. How can I deploy this application? You can deploy your application to platforms like Netlify, Vercel, or GitHub Pages. These platforms allow you to host static websites easily.
  3. What are some good resources for learning more TypeScript?
  4. How can I improve the UI? You can use CSS frameworks like Bootstrap or Tailwind CSS to improve the styling of your application. You can also use a UI library like React, Angular, or Vue.js to build a more complex and interactive user interface.

This tutorial provides a foundational understanding of building a budget tracker application with TypeScript. The concepts introduced here can be applied to a wide range of web development projects, allowing you to create more robust and maintainable applications. Remember to practice and experiment with the code to solidify your understanding and explore further features. The combination of TypeScript’s type safety and the structured approach to building the application will provide a solid base for your future web development endeavors.