TypeScript Tutorial: Building a Simple Web-Based Contact Manager

In today’s interconnected world, managing contacts efficiently is more crucial than ever. Whether you’re a freelancer juggling clients, a small business owner organizing leads, or simply someone trying to keep track of friends and family, a well-designed contact manager can be a game-changer. This tutorial will guide you through building a simple, yet functional, web-based contact manager using TypeScript. We’ll explore core concepts, write clean and maintainable code, and learn how to structure your application for scalability. By the end, you’ll have a practical project to showcase your TypeScript skills and a useful tool to manage your contacts.

Why TypeScript?

Before diving into the code, let’s address the elephant in the room: 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 provides several benefits:

  • Early Error Detection: TypeScript catches type-related errors during development, preventing runtime surprises.
  • Improved Code Readability: Types act as documentation, making your code easier to understand and maintain.
  • Enhanced Refactoring: TypeScript makes refactoring safer and more efficient, as the compiler helps you identify and fix potential issues.
  • Better Tooling: TypeScript provides excellent support in IDEs, including autocompletion, code navigation, and refactoring tools.

While JavaScript is a powerful language, TypeScript enhances it by bringing structure and predictability to your codebase. This is especially beneficial for larger projects, where maintaining code quality is paramount.

Setting Up the Project

Let’s get started by setting up our project. We’ll use Node.js and npm (Node Package Manager) for this tutorial. If you don’t have them installed, download and install them from the official Node.js website.

  1. Create a Project Directory: Create a new directory for your project (e.g., `contact-manager`).
  2. Initialize npm: Open your terminal, navigate to your project directory, and run `npm init -y`. This creates a `package.json` file, which manages your project’s dependencies.
  3. Install TypeScript: Install TypeScript as a development dependency by running `npm install –save-dev typescript`.
  4. Create a `tsconfig.json` file: This file configures the TypeScript compiler. Run `npx tsc –init` in your terminal. This creates a default `tsconfig.json` file with many configuration options. We’ll customize it later.
  5. Create Source Files: Create a `src` directory to hold your TypeScript source files. Inside `src`, create a file named `index.ts`.

Your project structure should look like this:

contact-manager/
├── package.json
├── tsconfig.json
└── src/
    └── index.ts

Configuring `tsconfig.json`

The `tsconfig.json` file controls how the TypeScript compiler behaves. Let’s customize it to suit our project. Open `tsconfig.json` and modify the following settings (you can leave the other settings as they are for now):

{
  "compilerOptions": {
    "target": "es5", // or "es6", "es2015", etc.
    "module": "commonjs", // or "esnext", "amd", "umd", etc.
    "outDir": "./dist", // Output directory for compiled JavaScript files
    "rootDir": "./src", // Root directory of your source files
    "strict": true, // Enable strict type checking
    "esModuleInterop": true, // Enables interoperability between CommonJS and ES Modules
    "skipLibCheck": true, // Skips type checking of declaration files
    "forceConsistentCasingInFileNames": true // Enforces consistent casing in filenames
  },
  "include": ["src/**/*"]
}

Let’s break down these settings:

  • `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 where the compiled JavaScript files will be placed. We’ll put them in a `dist` directory.
  • `rootDir`: Specifies the root directory of your TypeScript source files.
  • `strict`: Enables strict type checking, which is highly recommended for catching potential errors.
  • `esModuleInterop`: Helps with compatibility between different module systems.
  • `skipLibCheck`: Skips type checking of declaration files, improving compilation speed.
  • `forceConsistentCasingInFileNames`: Enforces consistent casing in filenames, which can prevent issues on case-sensitive file systems.
  • `include`: Specifies which files to include in the compilation.

Defining Contact Data

The core of our contact manager is the contact data itself. Let’s define a TypeScript interface to represent a contact. Open `src/index.ts` and add the following code:

// Define an interface for a Contact
interface Contact {
  id: number;
  firstName: string;
  lastName: string;
  email: string;
  phone?: string; // Optional property
  notes?: string; // Optional property
}

Here, we define an `interface` called `Contact`. An interface describes the structure of an object. This `Contact` interface specifies the properties each contact should have. Let’s break down the interface:

  • `id: number;`: Each contact will have a unique numerical identifier.
  • `firstName: string;`: The contact’s first name.
  • `lastName: string;`: The contact’s last name.
  • `email: string;`: The contact’s email address.
  • `phone?: string;`: The contact’s phone number. The `?` makes this property optional.
  • `notes?: string;`: Any additional notes about the contact. Also optional.

By using an interface, we ensure that all contact objects conform to a specific structure, making our code more predictable and less prone to errors. If you try to create a contact object that doesn’t match the interface, the TypeScript compiler will flag it as an error.

Implementing the Contact Manager Class

Now, let’s create a class to manage our contacts. This class will handle adding, retrieving, updating, and deleting contacts. Add the following code to `src/index.ts`:

// Define an interface for a Contact
interface Contact {
  id: number;
  firstName: string;
  lastName: string;
  email: string;
  phone?: string; // Optional property
  notes?: string; // Optional property
}

// Create a class to manage contacts
class ContactManager {
  private contacts: Contact[];
  private nextId: number;

  constructor() {
    this.contacts = [];
    this.nextId = 1;
  }

  // Add a new contact
  addContact(contact: Omit): Contact {
    const newContact: Contact = {
      id: this.nextId,
      ...contact,
    };
    this.contacts.push(newContact);
    this.nextId++;
    return newContact;
  }

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

  // Get a contact by ID
  getContactById(id: number): Contact | undefined {
    return this.contacts.find((contact) => contact.id === id);
  }

  // Update a contact
  updateContact(id: number, updates: Partial): Contact | undefined {
    const contactIndex = this.contacts.findIndex((contact) => contact.id === id);
    if (contactIndex === -1) {
      return undefined;
    }
    this.contacts[contactIndex] = {
      ...this.contacts[contactIndex],
      ...updates,
    };
    return this.contacts[contactIndex];
  }

  // Delete a contact
  deleteContact(id: number): boolean {
    const initialLength = this.contacts.length;
    this.contacts = this.contacts.filter((contact) => contact.id !== id);
    return this.contacts.length < initialLength;
  }
}

Let’s break down the `ContactManager` class:

  • `private contacts: Contact[];`: This private property stores an array of `Contact` objects. The `private` keyword ensures that this property can only be accessed from within the `ContactManager` class.
  • `private nextId: number;`: This private property keeps track of the next available ID for a new contact.
  • `constructor() { … }`: The constructor initializes the `contacts` array as empty and sets the `nextId` to 1.
  • `addContact(contact: Omit): Contact { … }`: This method adds a new contact to the `contacts` array. It takes a contact object as input and assigns a unique ID to it. The `Omit` type tells TypeScript that the `contact` argument should be a `Contact` object, but without the `id` property (because the `ContactManager` is responsible for generating the ID). It uses the spread operator (`…contact`) to merge the properties of the input `contact` with the new contact object.
  • `getContacts(): Contact[] { … }`: This method returns an array of all contacts.
  • `getContactById(id: number): Contact | undefined { … }`: This method retrieves a contact by its ID. It returns the contact object if found, or `undefined` if not found.
  • `updateContact(id: number, updates: Partial): Contact | undefined { … }`: This method updates an existing contact. It takes the contact’s ID and an object containing the updates. The `Partial` type allows you to update only some of the contact properties. It returns the updated contact object or `undefined` if the contact is not found.
  • `deleteContact(id: number): boolean { … }`: This method deletes a contact by its ID. It returns `true` if the contact was deleted, and `false` otherwise.

Implementing the UI (Basic HTML and JavaScript)

For this tutorial, we will create a very basic HTML and JavaScript UI to interact with our `ContactManager`. This will allow you to add, view, update, and delete contacts. Create an `index.html` file in the root directory of your project 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>Contact Manager</title>
    <style>
        body {
            font-family: sans-serif;
        }
        .contact-form, .contact-list {
            margin-bottom: 20px;
        }
        .contact-item {
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h2>Contact Manager</h2>

    <div class="contact-form">
        <h3>Add Contact</h3>
        <form id="add-contact-form">
            <label for="firstName">First Name:</label>
            <input type="text" id="firstName" name="firstName" required><br>

            <label for="lastName">Last Name:</label>
            <input type="text" id="lastName" name="lastName" required><br>

            <label for="email">Email:</label>
            <input type="email" id="email" name="email" required><br>

            <label for="phone">Phone:</label>
            <input type="tel" id="phone" name="phone"><br>

            <label for="notes">Notes:</label>
            <textarea id="notes" name="notes" rows="3"></textarea><br>

            <button type="submit">Add Contact</button>
        </form>
    </div>

    <div class="contact-list">
        <h3>Contacts</h3>
        <ul id="contact-list">
            <!-- Contacts will be displayed here -->
        </ul>
    </div>

    <script src="dist/index.js"></script>
</body>
</html>

This HTML provides a basic form for adding contacts and a list to display them. It also includes the compiled JavaScript file (`dist/index.js`) that we will generate shortly. Notice that we are including the compiled JavaScript file in the `<script>` tag at the end of the `body`.

Now, let’s create the JavaScript code that will interact with our `ContactManager` class. Add the following code to `src/index.ts` (replace the previous code in this file):


// Define an interface for a Contact
interface Contact {
  id: number;
  firstName: string;
  lastName: string;
  email: string;
  phone?: string; // Optional property
  notes?: string; // Optional property
}

// Create a class to manage contacts
class ContactManager {
  private contacts: Contact[];
  private nextId: number;

  constructor() {
    this.contacts = [];
    this.nextId = 1;
  }

  // Add a new contact
  addContact(contact: Omit): Contact {
    const newContact: Contact = {
      id: this.nextId,
      ...contact,
    };
    this.contacts.push(newContact);
    this.nextId++;
    return newContact;
  }

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

  // Get a contact by ID
  getContactById(id: number): Contact | undefined {
    return this.contacts.find((contact) => contact.id === id);
  }

  // Update a contact
  updateContact(id: number, updates: Partial): Contact | undefined {
    const contactIndex = this.contacts.findIndex((contact) => contact.id === id);
    if (contactIndex === -1) {
      return undefined;
    }
    this.contacts[contactIndex] = {
      ...this.contacts[contactIndex],
      ...updates,
    };
    return this.contacts[contactIndex];
  }

  // Delete a contact
  deleteContact(id: number): boolean {
    const initialLength = this.contacts.length;
    this.contacts = this.contacts.filter((contact) => contact.id !== id);
    return this.contacts.length  {
    const listItem = document.createElement('li');
    listItem.classList.add('contact-item');
    listItem.innerHTML = `
      <strong>${contact.firstName} ${contact.lastName}</strong><br>
      Email: ${contact.email}<br>
      Phone: ${contact.phone || 'N/A'}<br>
      Notes: ${contact.notes || 'N/A'}<br>
      <button data-id="${contact.id}" class="delete-button">Delete</button>
    `;

    // Add event listener for delete button
    const deleteButton = listItem.querySelector('.delete-button') as HTMLButtonElement;
    deleteButton.addEventListener('click', () => {
      const contactId = parseInt(deleteButton.dataset.id || '', 10);
      if (contactId !== undefined && !isNaN(contactId)) {
        if (contactManager.deleteContact(contactId)) {
          renderContacts(); // Refresh the list after deletion
        }
      }
    });

    contactList.appendChild(listItem);
  });
}

// Event listener for the add contact form
addContactForm.addEventListener('submit', (event) => {
  event.preventDefault(); // Prevent the default form submission

  // Get the form values
  const firstName = (document.getElementById('firstName') as HTMLInputElement).value;
  const lastName = (document.getElementById('lastName') as HTMLInputElement).value;
  const email = (document.getElementById('email') as HTMLInputElement).value;
  const phone = (document.getElementById('phone') as HTMLInputElement).value;
  const notes = (document.getElementById('notes') as HTMLTextAreaElement).value;

  // Create a new contact object
  const newContact: Omit = {
    firstName,
    lastName,
    email,
    phone,
    notes,
  };

  // Add the contact to the ContactManager
  contactManager.addContact(newContact);

  // Render the updated contact list
  renderContacts();

  // Reset the form
  addContactForm.reset();
});

// Initial render of contacts
renderContacts();

This JavaScript code does the following:

  • Instantiates the `ContactManager` class: Creates an instance of our contact manager to manage contacts.
  • Gets references to HTML elements: Gets references to the form and the contact list in the HTML.
  • `renderContacts()` function: This function is responsible for displaying the contacts in the UI. It clears the existing list, fetches the contacts from the `ContactManager`, and creates list items for each contact, then appends them to the contact list. It also attaches delete button event listeners.
  • Event listener for the add contact form: This code adds an event listener to the form to handle the submission of new contact data. When the form is submitted, it prevents the default form submission behavior, gets the form values, creates a new contact object, adds it to the `ContactManager`, renders the updated contact list, and resets the form.
  • Initial render of contacts: Calls `renderContacts()` to initially display any existing contacts (though, in this case, there won’t be any initially).

Compiling and Running the Application

Now that we have our TypeScript code and HTML, let’s compile the TypeScript code into JavaScript and run the application.

  1. Compile the TypeScript code: In your terminal, run `npx tsc`. This will compile your `index.ts` file into `index.js` in the `dist` directory.
  2. Open `index.html` in your browser: Navigate to your project directory and open the `index.html` file in your web browser.

You should see the basic UI of the contact manager. You can now add contacts, and they will be displayed in the list. When you click the “Delete” button, that contact will be removed from the list.

Common Mistakes and How to Fix Them

When working with TypeScript, especially for beginners, you might encounter some common mistakes. Here’s a list of these mistakes and how to fix them:

  • Type Errors:
    • Mistake: Trying to assign a value of the wrong type to a variable. For example, assigning a string to a number variable.
    • Fix: Carefully review the error message provided by the TypeScript compiler. The error message will tell you the expected type and the type you provided. Ensure the types match or convert the value to the correct type.
  • Incorrect Module Imports/Exports:
    • Mistake: Not importing modules correctly or exporting variables/classes incorrectly.
    • Fix: Double-check your import and export statements. Make sure you are using the correct syntax (e.g., `import { MyClass } from ‘./my-module’;`) and that you have exported the necessary variables/classes from the module you are importing.
  • Ignoring Compiler Errors:
    • Mistake: Ignoring the errors and warnings from the TypeScript compiler.
    • Fix: The compiler is your friend! Always fix the errors and warnings the compiler provides. They are there to help you write better and more maintainable code.
  • Incorrectly Using Optional Properties:
    • Mistake: Assuming an optional property always has a value.
    • Fix: Always check if an optional property exists before accessing it. Use the optional chaining operator (`?.`) or the nullish coalescing operator (`??`) to handle cases where the property might be `undefined` or `null`.
  • Not Using Interfaces Effectively:
    • Mistake: Not using interfaces to define the structure of your objects.
    • Fix: Use interfaces to define the shape of your objects. This will help you catch type errors early and make your code more readable and maintainable.

Key Takeaways

  • TypeScript Enhances JavaScript: TypeScript adds static typing to JavaScript, improving code quality and maintainability.
  • Interfaces Define Structure: Interfaces are essential for defining the structure of your objects, ensuring type safety.
  • Classes Encapsulate Logic: Classes help organize your code and encapsulate related data and methods.
  • Modular Design is Key: Breaking your application into smaller, reusable components makes it easier to manage and scale.
  • Testing is Important: While not covered in this basic tutorial, always write tests to ensure your code works as expected.

FAQ

  1. Can I use this contact manager in a real-world application?

    Yes, you can certainly adapt this code for a real-world application. However, you’ll likely want to add features like data persistence (e.g., storing contacts in a database or local storage), more advanced UI components, and error handling. You would also want to consider using a framework like React, Angular, or Vue.js for a more robust and scalable front-end.

  2. How can I add data persistence to the contact manager?

    You can use various methods to store the contact data. For a simple solution, you could use the browser’s local storage to save the contacts. For a more robust solution, you could connect to a database (e.g., using Node.js and a database like MongoDB or PostgreSQL) and store the contacts there. This would allow you to store and retrieve data even if the user closes their browser.

  3. How can I improve the UI?

    The current UI is very basic. You can improve it by:

    • Using a CSS framework like Bootstrap or Tailwind CSS to quickly style your application.
    • Adding features like contact editing, searching, and sorting.
    • Using a front-end framework like React, Angular, or Vue.js to create a more interactive and dynamic user interface.
  4. How do I handle errors in a real-world application?

    In a real-world application, you should handle errors gracefully. This includes:

    • Using `try…catch` blocks to catch potential errors.
    • Displaying informative error messages to the user.
    • Logging errors to a server-side log for debugging.
    • Implementing input validation to prevent invalid data from being entered.

This tutorial has provided a solid foundation for building a web-based contact manager with TypeScript. We have covered the essentials, from setting up the project to building the core functionality and creating a basic UI. You can now expand on this foundation, adding features, improving the UI, and integrating data persistence. Remember to practice, experiment, and continue learning to master TypeScript and web development. With each project, you’ll gain a deeper understanding of the language and its capabilities, enabling you to build increasingly complex and sophisticated applications.