TypeScript Tutorial: Building a Simple Address Book Application

In the digital age, managing contacts efficiently is crucial. Whether you’re a freelancer, a small business owner, or simply someone with a lot of connections, keeping track of names, phone numbers, and addresses can be a real headache. Imagine a scenario: you need to quickly find a client’s phone number, but you’re stuck sifting through a messy spreadsheet or a cluttered phone. This is where a well-designed address book application comes to the rescue. This tutorial will guide you through building a simple, yet functional, address book using TypeScript, a superset of JavaScript that adds static typing. We’ll cover the fundamental concepts, from setting up your development environment to implementing features like adding, editing, deleting, and searching contacts. By the end of this tutorial, you’ll have a solid understanding of TypeScript and a practical application to show for it.

Setting Up Your Development Environment

Before diving into the code, let’s set up the necessary tools. You’ll need Node.js and npm (Node Package Manager) installed on your system. If you haven’t already, you can download them from the official Node.js website. Once installed, verify the installation by running the following commands in your terminal:

node -v
npm -v

These commands should display the installed versions of Node.js and npm. Next, we’ll install TypeScript globally using npm:

npm install -g typescript

This command installs the TypeScript compiler globally, allowing you to compile TypeScript files from any directory in your terminal. To confirm the installation, run:

tsc -v

This should display the TypeScript compiler version. Now, let’s create a project directory for our address book:

mkdir address-book
cd address-book

Inside the project directory, we’ll initialize a new npm project:

npm init -y

This command creates a `package.json` file, which manages our project’s dependencies and scripts. Next, we’ll create a `tsconfig.json` file. This file configures the TypeScript compiler. Run the following command to generate a basic `tsconfig.json`:

tsc --init

This command creates a `tsconfig.json` file with default settings. We’ll customize this file later. For now, we have everything set up to start writing TypeScript code.

Defining Data Structures with TypeScript

TypeScript’s static typing is one of its most significant advantages. It allows us to define the structure of our data, making our code more predictable and easier to maintain. For our address book, we’ll need a data structure to represent a contact. Let’s define an interface called `Contact`:

interface Contact {
  firstName: string;
  lastName: string;
  phoneNumber: string;
  email?: string; // Optional property
  address?: {
    street: string;
    city: string;
    state: string;
    zipCode: string;
  };
}

In this interface:

  • `firstName`, `lastName`, and `phoneNumber` are required string properties.
  • `email` is an optional string property (indicated by the `?`).
  • `address` is an optional object property with nested properties for street, city, state, and zip code.

Now, let’s create a class called `AddressBook` to manage our contacts:

class AddressBook {
  private contacts: Contact[] = [];

  addContact(contact: Contact): void {
    this.contacts.push(contact);
  }

  getContacts(): Contact[] {
    return this.contacts;
  }

  findContact(searchTerm: string): Contact[] {
    searchTerm = searchTerm.toLowerCase();
    return this.contacts.filter(contact =>
      contact.firstName.toLowerCase().includes(searchTerm) ||
      contact.lastName.toLowerCase().includes(searchTerm) ||
      contact.phoneNumber.includes(searchTerm) ||
      (contact.email && contact.email.toLowerCase().includes(searchTerm))
    );
  }

  deleteContact(phoneNumber: string): void {
    this.contacts = this.contacts.filter(contact => contact.phoneNumber !== phoneNumber);
  }

  editContact(phoneNumber: string, updatedContact: Partial<Contact>): void {
    const index = this.contacts.findIndex(contact => contact.phoneNumber === phoneNumber);
    if (index !== -1) {
      this.contacts[index] = { ...this.contacts[index], ...updatedContact };
    }
  }
}

In this class:

  • `contacts`: An array of `Contact` objects to store our contacts. It’s declared as `private`, meaning it can only be accessed from within the `AddressBook` class.
  • `addContact(contact: Contact)`: Adds a new contact to the address book.
  • `getContacts(): Contact[]`: Returns the array of contacts.
  • `findContact(searchTerm: string)`: Searches for contacts based on the search term (first name, last name, phone number, or email).
  • `deleteContact(phoneNumber: string)`: Deletes a contact based on their phone number.
  • `editContact(phoneNumber: string, updatedContact: Partial<Contact>)`: Edits an existing contact. It uses `Partial<Contact>` to allow only specific properties of the contact to be updated.

Implementing the Address Book Functionality

Now, let’s put our `AddressBook` class into action. We’ll create a simple command-line interface (CLI) to interact with the address book. Create a file named `index.ts` in your project directory. This file will contain the main logic of our application:

import { AddressBook, Contact } from './address-book'; // Assuming you save the AddressBook class in address-book.ts
import * as readline from 'readline';

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const addressBook = new AddressBook();

function askQuestion(query: string): Promise<string> {
  return new Promise(resolve => rl.question(query, resolve));
}

async function addContact() {
  const firstName = await askQuestion('First Name: ');
  const lastName = await askQuestion('Last Name: ');
  const phoneNumber = await askQuestion('Phone Number: ');
  const email = await askQuestion('Email (optional): ');

  const contact: Contact = {
    firstName,
    lastName,
    phoneNumber,
    email,
  };

  addressBook.addContact(contact);
  console.log('Contact added successfully!');
  mainMenu();
}

async function viewContacts() {
  const contacts = addressBook.getContacts();
  if (contacts.length === 0) {
    console.log('Address book is empty.');
  } else {
    contacts.forEach(contact => {
      console.log(`
First Name: ${contact.firstName}`);
      console.log(`Last Name: ${contact.lastName}`);
      console.log(`Phone Number: ${contact.phoneNumber}`);
      if (contact.email) {
        console.log(`Email: ${contact.email}`);
      }
    });
  }
  mainMenu();
}

async function searchContacts() {
  const searchTerm = await askQuestion('Enter search term: ');
  const results = addressBook.findContact(searchTerm);
  if (results.length === 0) {
    console.log('No contacts found.');
  } else {
    results.forEach(contact => {
      console.log(`
First Name: ${contact.firstName}`);
      console.log(`Last Name: ${contact.lastName}`);
      console.log(`Phone Number: ${contact.phoneNumber}`);
      if (contact.email) {
        console.log(`Email: ${contact.email}`);
      }
    });
  }
  mainMenu();
}

async function deleteContact() {
  const phoneNumber = await askQuestion('Enter phone number to delete: ');
  addressBook.deleteContact(phoneNumber);
  console.log('Contact deleted successfully!');
  mainMenu();
}

async function editContact() {
  const phoneNumber = await askQuestion('Enter phone number to edit: ');
  const contactToEdit = addressBook.getContacts().find(contact => contact.phoneNumber === phoneNumber);

  if (!contactToEdit) {
    console.log('Contact not found.');
    mainMenu();
    return;
  }

  const newFirstName = await askQuestion(`New First Name (${contactToEdit.firstName}): `) || contactToEdit.firstName;
  const newLastName = await askQuestion(`New Last Name (${contactToEdit.lastName}): `) || contactToEdit.lastName;
  const newPhoneNumber = await askQuestion(`New Phone Number (${contactToEdit.phoneNumber}): `) || contactToEdit.phoneNumber;
  const newEmail = await askQuestion(`New Email (${contactToEdit.email || ''}): `) || contactToEdit.email;

  const updatedContact: Partial<Contact> = {
    firstName: newFirstName,
    lastName: newLastName,
    phoneNumber: newPhoneNumber,
    email: newEmail,
  };

  addressBook.editContact(phoneNumber, updatedContact);
  console.log('Contact updated successfully!');
  mainMenu();
}

async function mainMenu() {
  console.log('nAddress Book Menu:');
  console.log('1. Add Contact');
  console.log('2. View Contacts');
  console.log('3. Search Contacts');
  console.log('4. Delete Contact');
  console.log('5. Edit Contact');
  console.log('6. Exit');

  const choice = await askQuestion('Enter your choice: ');

  switch (choice) {
    case '1':
      await addContact();
      break;
    case '2':
      await viewContacts();
      break;
    case '3':
      await searchContacts();
      break;
    case '4':
      await deleteContact();
      break;
    case '5':
      await editContact();
      break;
    case '6':
      rl.close();
      break;
    default:
      console.log('Invalid choice. Please try again.');
      mainMenu();
  }
}

mainMenu();

In this file:

  • We import the `AddressBook` class and the `Contact` interface.
  • We use the `readline` module to create a simple CLI.
  • The `askQuestion` function is a helper function to prompt the user for input.
  • The `addContact`, `viewContacts`, `searchContacts`, `deleteContact`, and `editContact` functions implement the core functionality of our address book.
  • The `mainMenu` function displays the menu and handles user input.

To make this code runnable, save the `AddressBook` class and `Contact` interface in a file named `address-book.ts` in the same directory as `index.ts`. Now, let’s compile and run the application. First, compile the TypeScript files:

tsc

This command will compile all `.ts` files in your project directory and generate corresponding `.js` files. Finally, run the application:

node index.js

You should now see the address book menu in your terminal. You can add, view, search, delete, and edit contacts using the CLI.

Handling Common Mistakes

When working with TypeScript, and programming in general, you’ll inevitably encounter errors. Here are some common mistakes and how to avoid them:

Type Errors

TypeScript’s type system is designed to catch errors early. If you see type errors during compilation, carefully review the error messages. They usually indicate where the problem lies. For example, if you try to assign a number to a string variable, TypeScript will flag it. To fix this, ensure that your data types match the expected types in your interfaces and classes.

Incorrect Imports

Make sure your import statements are correct. Double-check the file paths and the names of the exported classes, interfaces, and functions. Incorrect imports can lead to runtime errors or unexpected behavior. Use your IDE’s auto-completion features to help with imports and avoid typos.

Uninitialized Variables

In TypeScript, it’s good practice to initialize variables when you declare them. If you don’t initialize a variable, it might have an undefined value, which can lead to errors. If a value is not immediately available, you can assign it a default value (e.g., `let myVariable: string = ”;`).

Incorrect Scope

Pay attention to the scope of your variables. Variables declared inside a function are only accessible within that function. If you try to access a variable outside its scope, you’ll get an error. Use the appropriate scope (e.g., `let`, `const`, `var`, `private`, `public`) to control the visibility of your variables.

Asynchronous Operations

When working with asynchronous operations (e.g., network requests, file I/O), make sure to handle them correctly. Use `async/await` to write more readable asynchronous code, and handle potential errors with `try/catch` blocks. If you are using callbacks, be mindful of callback hell and consider using Promises or async/await to simplify your code.

Improving the Address Book Application

The address book application we built is functional, but it can be improved. Here are some ideas for enhancements:

  • Data Persistence: Currently, the contacts are stored in memory and are lost when the application closes. Implement data persistence using a file (e.g., JSON file) or a database (e.g., SQLite) to store the contacts permanently.
  • Error Handling: Implement more robust error handling. For example, validate user input to prevent invalid data from being entered. Handle potential errors during file I/O or database operations.
  • User Interface: Instead of a command-line interface, create a graphical user interface (GUI) using a framework like React, Angular, or Vue.js to provide a more user-friendly experience.
  • Advanced Search: Implement more advanced search features, such as searching by multiple criteria (e.g., first name AND last name).
  • Sorting and Filtering: Add the ability to sort and filter contacts based on different criteria (e.g., by first name, last name, or phone number).
  • Import/Export: Allow users to import contacts from a CSV file or export contacts to a CSV or other format.
  • Address Book Groups: Allow users to create groups of contacts, making it easier to manage large contact lists.

Key Takeaways

  • TypeScript Fundamentals: You’ve learned about interfaces, classes, types, and how to use them to structure your data and write more maintainable code.
  • Building a CLI Application: You’ve gained experience in creating a simple command-line interface using the `readline` module.
  • Error Handling: You’ve learned about common mistakes and how to avoid them, improving your debugging skills.
  • Code Organization: You’ve seen how to organize your code into separate files and modules for better readability and maintainability.

FAQ

  1. Why use TypeScript instead of JavaScript? TypeScript adds static typing to JavaScript, which helps catch errors early, improves code readability, and makes your code more maintainable. It also offers features like interfaces and classes, which can help you structure your code better.
  2. How do I compile TypeScript code? You compile TypeScript code using the `tsc` command-line tool. The `tsc` tool translates your TypeScript code into JavaScript code that can be run in any JavaScript environment (e.g., a web browser or Node.js).
  3. What is the difference between `interface` and `class` in TypeScript? An `interface` defines the structure of an object (its properties and their types), but it doesn’t contain any implementation. A `class` is a blueprint for creating objects, and it can contain both properties and methods (implementation).
  4. How can I handle user input in a CLI application? You can use the `readline` module in Node.js to read user input from the command line. The `readline.createInterface()` method creates an interface for interacting with the user, and the `rl.question()` method prompts the user for input.
  5. How can I improve the performance of my address book application? Consider using data structures that are optimized for searching and sorting (e.g., a hash map or a sorted array). If you’re using a database, ensure that you have appropriate indexes on your data. Optimize your code to avoid unnecessary operations. For a CLI application, performance is usually not a major concern unless you are dealing with a very large number of contacts.

Building an address book application with TypeScript is a great way to learn the fundamentals of the language and practice your coding skills. From setting up your development environment to implementing features like adding, editing, and searching contacts, you’ve gained practical experience in building a real-world application. As you continue to develop your skills, explore the enhancements suggested above to make your address book even more powerful and feature-rich. Remember, the key to mastering any programming language is to practice consistently and to apply your knowledge to solve real-world problems. With TypeScript, you have a powerful tool at your disposal to build robust and maintainable applications.