Build a Contact Book CLI in TypeScript

Build a type-safe CLI app to learn TypeScript interfaces, unions, and classes.

Introduction

30 Second Summary

Every developer has dealt with a frustrating bug that only shows up when the code runs. A misspelled property name, a number passed where a string was expected, or a missing field that crashes everything. These bugs hide in plain sight until a user triggers them.

In this project, you will build a command-line Contact Book application in TypeScript that catches these bugs before your code even runs. You will define typed interfaces, build a class with type-safe methods, and see the TypeScript compiler reject invalid code in real time.

What You'll Build

A fully typed Contact Book CLI that lets you add, search, list, and delete contacts with compile-time type safety.

By the end of this project, you'll have:

  • A configured TypeScript project with tsconfig.json and strict mode enabled.
  • A Contact interface and union types that model your data with precision.
  • A ContactBook class with type-safe CRUD methods that the compiler verifies for you.
  • Secret Mission: Add a favorites feature using generics and category filtering for reusable, type-safe utility functions.

Are there any prerequisites?

You need a basic understanding of JavaScript (variables, functions, arrays, objects). No prior TypeScript experience is required.

Set Up Your TypeScript Environment

TypeScript needs a runtime to compile and execute your code.

Node.js provides that runtime, and its package manager (npm) lets you install TypeScript itself. In this step, you'll get Node.js running, create your project folder, and install TypeScript so everything is ready for writing type-safe code.

In this step, get ready to:
  • Install Node.js v24 LTS and verify it's working.
  • Create your project folder and install TypeScript.
  • Generate a TypeScript configuration file.
Install Node.js and verify it's ready

Node.js is a JavaScript runtime that lets you run code outside of a browser. TypeScript's compiler runs on Node.js, so you need it installed first.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Check if Node.js is already installed by running this command:
node --version

✔️ I see version 24 or higher

Nice, Node.js is ready. You should see something like v24.15.0 (the exact patch number may differ).

  • Verify npm is available by running:
npm --version

You should see a version number printed. npm comes bundled with Node.js, so if Node is working, npm will be too.

ⓧ I see an older version

You have Node.js installed, but your version is older than v24. This project uses features available in the v24 LTS release, so you need to upgrade.

  • Visit the Node.js download page.
  • Download the LTS installer for your operating system (v24).
  • Run the installer and follow the prompts. It will replace your current version.
  • Close and reopen your terminal, then run node --version again to confirm it shows v24 or higher.

Already use nvm?

If you manage Node versions with nvm, run these commands instead:

nvm install 24
nvm use 24

ⓧ Command not found

Node.js is not installed on your machine yet. Let's fix that.

  • Visit the Node.js download page.
  • Download the LTS installer for your operating system (v24).
  • Run the installer and follow all the default prompts.
  • Close and reopen your terminal, then run node --version to confirm you see v24 or higher.
  • Run npm --version to confirm npm was installed alongside Node.js.

Still seeing command not found after installing?

Make sure you closed and reopened your terminal after the install. Your terminal needs to reload its PATH to find the new node binary.

On Windows, if you installed via the MSI, try opening a new Command Prompt window (not the same one). On macOS, open a fresh Terminal tab.

help me troubleshoot Node.js not being found after installation

Create your project folder and install TypeScript

Every TypeScript project lives in its own folder with a package.json file that tracks dependencies. You'll create that folder now and install TypeScript into it.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir contact-book
cd contact-book
  • Confirm the folder exists by running:
ls ~/Desktop

You should see contact-book listed in the output.

  • Initialize a new Node.js project by running:
npm init -y

What does this command do?

The -y flag tells npm to accept all default values. This creates a package.json file that tracks your project's name, version, and dependencies.

You should see output confirming that package.json was created.

  • Install TypeScript as a dev dependency by running:
npm install -D typescript@6.0.3

What does this command do?

The -D flag marks TypeScript as a development dependency. This means it's needed for building your project, but not for running the final JavaScript output.

Pinning the version to 6.0.3 ensures everyone following this project uses the same TypeScript version and gets the same behaviour.

You should see npm download TypeScript and print a summary of added packages.

Seeing permission errors or network failures?

On macOS, if you see EACCES errors, your global npm directory has a permissions issue. Running the install inside your project folder (as you're doing here) avoids this for local installs.

If the install hangs or fails with a network error, check your internet connection and try running the command again.

help me fix npm install errors for TypeScript

✔️ Awesome, I've got everything!

Great. Make sure you're still inside the contact-book folder before continuing to the next substep.

ⓧ I'd like to double check the full code

Open package.json and compare it to this:

{
  "name": "contact-book",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "typescript": "6.0.3"
  }
}

The key thing to confirm is that typescript appears under devDependencies with version 6.0.3.

Generate your TypeScript configuration

TypeScript uses a tsconfig.json file to control how it compiles your code. The TypeScript compiler can generate a starter configuration for you automatically.

  • Generate a default tsconfig.json by running:
npx tsc --init

What does this command do?

The npx prefix runs the locally installed TypeScript compiler (tsc) without needing a global install. The --init flag tells it to create a new tsconfig.json with sensible defaults and helpful comments explaining each option.

You should see a message confirming that tsconfig.json was created successfully.

  • Verify the TypeScript compiler version by running:
npx tsc --version

You should see Version 6.0.3 printed in your terminal. This confirms TypeScript is installed correctly and ready to compile your code.

Not seeing version 6.0.3?

If you see a different version, make sure you ran npm install -D typescript@6.0.3 in the previous substep. The npx command uses the locally installed version, so it should match what you installed.

If you see a "command not found" error, double check that you're inside the contact-book folder where you ran the install.

help me troubleshoot my TypeScript compiler version

Your environment is ready. Next up, you'll configure TypeScript's settings and write your first line of typed code.

Configure Your Project and Write Your First TypeScript

You have Node.js, npm, and the TypeScript compiler installed in your project folder. Now it's time to tell TypeScript how to behave when it compiles your code.

TypeScript's behavior is controlled by a configuration file called tsconfig.json. In this step, you'll set it up, install the type definitions Node.js needs, and write your very first TypeScript file.

In this step, get ready to:
  • Configure your TypeScript project settings in tsconfig.json.
  • Install Node.js type definitions and write your first TypeScript file.
  • Compile TypeScript to JavaScript and run it.
Replace the default tsconfig.json

The default tsconfig.json generated by npx tsc --init is full of comments and options you don't need yet. You'll replace it with a clean configuration tailored to this project.

  • Open tsconfig.json in your code editor.
  • Select all the existing content and delete it.
  • Paste in the following configuration:
{
  "compilerOptions": {
    "target": "es2025",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "types": ["node"]
  },
  "include": ["src/**/*"]
}

What do these settings do?

  • outDir tells TypeScript to put compiled JavaScript files into a dist/ folder, keeping your project tidy.
  • rootDir tells TypeScript that all your source files live inside src/.
  • module and moduleResolution set to nodenext enable modern ESM (EcmaScript Module) imports that work with Node.js.
  • strict enables all of TypeScript's strictest type-checking rules. This catches the most bugs.
  • types set to ["node"] tells TypeScript to include Node.js type definitions (which you'll install shortly).
  • Save tsconfig.json.

You won't see any output from this change yet. The compiler will use these settings when you compile in the final substep.

✔️ Awesome, I've got everything!

Great. Make sure you've saved tsconfig.json before moving on.

ⓧ I'd like to double check the full code

{
  "compilerOptions": {
    "target": "es2025",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "types": ["node"]
  },
  "include": ["src/**/*"]
}
Install type definitions and create your first TypeScript file

TypeScript needs type information to understand Node.js built-in functions like console.log. The @types/node package provides these definitions.

Before installing, you need to update package.json to enable modern ESM modules and add helpful build scripts.

  • Open package.json in your code editor.
  • Replace all the content with the following:
{
  "name": "contact-book",
  "version": "1.0.0",
  "description": "A type-safe contact book CLI built with TypeScript",
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "devDependencies": {
    "typescript": "6.0.3"
  }
}

What changed in package.json?

  • "type": "module" tells Node.js to treat .js files as ES modules (matching the nodenext setting in your tsconfig).
  • "build": "tsc" gives you a shortcut to compile your TypeScript.
  • "start": "node dist/index.js" gives you a shortcut to run the compiled output.
  • "main": "dist/index.js" points to the compiled JavaScript entry point instead of the raw TypeScript source.
  • Save package.json.

You won't see a change yet. The next command will update your devDependencies with the new type definitions.

  • Install the Node.js type definitions by running this command in your terminal:
npm install -D @types/node

What does this command do?

The @types/node package tells TypeScript about every built-in Node.js function, object, and module. Without it, TypeScript would flag console.log as an unknown function.

  • You should see npm output confirming the package was added.

Seeing permission errors?

Make sure your terminal is still in the contact-book folder. Run pwd (macOS/Linux) or cd (Windows) to check your current location.

If you see a permission error on macOS, try prefixing the command with sudo.

help me fix npm permission errors when installing @types/node

Now create the folder where your TypeScript source files will live.

  • Create the src directory by running:
mkdir src
  • Confirm it was created by running:
ls
  • You should see src listed alongside node_modules, package.json, and tsconfig.json.

Time to write your first line of TypeScript.

  • Create a new file called index.ts inside the src folder.
  • Add the following code to src/index.ts:
const greeting: string = "Hello from TypeScript!";
console.log(greeting);

What is a type annotation?

The : string after greeting is a type annotation. It tells TypeScript that this variable must always hold a string value.

If you later tried to assign a number to greeting, TypeScript would catch the mistake before your code runs. This is the core benefit of TypeScript over plain JavaScript.

  • Save src/index.ts.

You won't see a change yet. The next substep compiles this file into runnable JavaScript.

✔️ Awesome, I've got everything!

Great. Double check you've saved both package.json and src/index.ts.

ⓧ I'd like to double check the full code

{
  "name": "contact-book",
  "version": "1.0.0",
  "description": "A type-safe contact book CLI built with TypeScript",
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "devDependencies": {
    "typescript": "6.0.3",
    "@types/node": "22.15.17"
  }
}
const greeting: string = "Hello from TypeScript!";
console.log(greeting);
Compile and run your TypeScript

TypeScript doesn't run directly in Node.js. You need to compile it into JavaScript first using the tsc command.

  • Compile your TypeScript by running:
npx tsc

What does this command do?

The npx tsc command runs the TypeScript compiler. It reads your tsconfig.json settings, finds all .ts files in src/, and outputs compiled .js files into dist/.

If compilation succeeds, you won't see any terminal output. Silence means success.

  • Confirm the compiled file exists by running:
ls dist
  • You should see index.js in the output.

Seeing a compilation error?

If you see an error mentioning rootDir, make sure your index.ts file is inside the src/ folder, not in the project root.

If you see an error about console not being found, double check that "types": ["node"] is in your tsconfig.json and that you installed @types/node.

help me fix TypeScript compilation errors in my contact-book project

  • Run the compiled JavaScript by running:
node dist/index.js
  • You should see Hello from TypeScript! printed in your terminal.

Don't see the greeting?

Make sure the path is dist/index.js (not src/index.ts). Node.js can only run the compiled .js file, not the .ts source.

If you see a module-related error, verify that "type": "module" is present in your package.json.

help me fix errors when running node dist/index.js in my TypeScript project

Your TypeScript environment is fully configured and working. Next up, you'll define your own custom types to model real contact data.

Define Your Contact Interface and Union Types

Your TypeScript project is configured and compiling successfully. Now it's time to define the shape of your data.

TypeScript's real power shows up when you describe your data with types. Instead of hoping your contact objects have the right fields, you'll define an interface that enforces the structure and a union type that restricts categories to an exact set of allowed values.

In this step, get ready to:
  • Define a union type and interface for your contact data.
  • Import the types and create a sample contact object.
  • See TypeScript catch a type error at compile time.
Create your types file

A dedicated types file keeps your type definitions organized and reusable across multiple files.

  • Create a new file called src/types.ts inside your src folder by running:
touch src/types.ts
  • Open src/types.ts in your editor and add the following code:
export type ContactCategory = "personal" | "work" | "family";

export interface Contact {
  id: number;
  name: string;
  phone: string;
  email?: string;
  category: ContactCategory;
}

What does this code do?

ContactCategory is a union type that restricts the value to exactly three strings: "personal", "work", or "family". Any other string will be rejected by the compiler.

The Contact interface defines the exact shape of a contact object. Every contact must have an id, name, phone, and category. The email field has a ? making it optional.

  • Save src/types.ts.
  • Confirm the file exists by running:
ls src/

You should see both index.ts and types.ts listed.

Don't see types.ts?

Make sure you ran the touch command from your project's root folder (the one containing package.json). If you're in the wrong directory, src/types.ts will be created elsewhere.

help me find where my types.ts file was created

✔️ Awesome, I've got everything!

Great. Double check you've saved the file before moving on.

ⓧ I'd like to double check the full code

export type ContactCategory = "personal" | "work" | "family";

export interface Contact {
  id: number;
  name: string;
  phone: string;
  email?: string;
  category: ContactCategory;
}
Import your types and introduce a type error

Now you'll use these types in your main file and see what happens when you break them.

  • Open src/index.ts and replace its entire contents with the following code:
import { Contact, ContactCategory } from "./types.js";

const sampleContact: Contact = {
  id: 1,
  name: "Alice Johnson",
  phone: "555-0101",
  email: "alice@company.com",
  category: "friend",
};

console.log(sampleContact);

What's happening here?

The import pulls Contact and ContactCategory from ./types.js. Then sampleContact is typed as Contact, meaning TypeScript will check every field against the interface.

The category is set to "friend" on purpose. This is not one of the three allowed values in ContactCategory, so TypeScript should reject it.

Why does the import say .js when the file is .ts?

With module set to nodenext, TypeScript requires import paths to use the .js extension. This matches the compiled output file that Node.js will actually load at runtime. You write ./types.js even though the source file is types.ts.

  • Save src/index.ts.
  • Try to compile by running:
npx tsc

You should see a compiler error indicating that "friend" is not assignable to type ContactCategory.

What is this error telling you?

TypeScript caught a bug before your code ever ran. The union type ContactCategory only allows "personal", "work", or "family". Since "friend" isn't in that list, the compiler refuses to continue.

In plain JavaScript, this typo would silently pass and only cause problems at runtime. TypeScript catches it immediately.

Don't see a compiler error?

Make sure you saved src/index.ts before running npx tsc. Also confirm the category value is spelled exactly "friend" (not one of the valid options). If npx tsc succeeds silently, the error was already fixed.

help me figure out why TypeScript is not showing a compiler error

Fix the error and compile successfully

Now that you've seen TypeScript catch the bug, fix it by using a valid category value.

  • In src/index.ts, find this line:
  category: "friend",
  • Replace "friend" with "work", so the line now looks like this:
  category: "work",
  • Save src/index.ts and compile again by running:
npx tsc

This time the compiler finishes without errors.

  • Run the compiled output to see your contact printed:
node dist/index.js

You should see the full contact object logged to the terminal, including all the fields you defined.

Still seeing a compiler error?

Check that your category value is exactly "work" with the quotes. Also confirm the import path ends with .js (not .ts). TypeScript with nodenext module resolution requires the .js extension in imports.

help me fix my TypeScript compiler error in index.ts

✔️ Awesome, I've got everything!

Great. Double check you've saved both files before moving on.

ⓧ I'd like to double check the full code

export type ContactCategory = "personal" | "work" | "family";

export interface Contact {
  id: number;
  name: string;
  phone: string;
  email?: string;
  category: ContactCategory;
}
import { Contact, ContactCategory } from "./types.js";

const sampleContact: Contact = {
  id: 1,
  name: "Alice Johnson",
  phone: "555-0101",
  email: "alice@company.com",
  category: "work",
};

console.log(sampleContact);

You just defined your first interface and union type, and experienced TypeScript catching a bug that JavaScript would have missed entirely. Next up, you'll build a full ContactBook class that uses these types for all its operations.

Build the ContactBook Class

Your types are defined and ready to use. Now it's time to build the class that actually manages your contacts.

In this step, you'll create a ContactBook class that stores contacts in a typed array and provides methods to add, list, search, and delete them. Every method parameter and return value is explicitly typed, so TypeScript can catch mistakes the moment you make them.

In this step, get ready to:
  • Create a ContactBook class with typed private fields.
  • Implement addContact and listContacts methods with full type annotations.
  • Implement findByName and deleteContact using typed array methods.
Create the ContactBook class skeleton

The ContactBook class is where all your contact management logic lives. It needs a private array to store contacts and a counter to assign unique IDs.

  • Create a new file called src/contactBook.ts and add the class skeleton with its imports and private fields:
import { Contact, ContactCategory } from "./types.js";

// ContactBook manages a collection of typed contacts
export class ContactBook {
  private contacts: Contact[] = [];
  private nextId: number = 1;
}

What does this code do?

The import line pulls in your Contact interface and ContactCategory type from the file you created in the previous step. Notice the .js extension in the import path. TypeScript with nodenext module resolution requires this, even though the source file is .ts.

The private keyword means only code inside the class can access these fields directly. This prevents outside code from bypassing your methods and corrupting the data.

  • Save src/contactBook.ts.
  • Confirm the class compiles by running:
npx tsc

If there are no errors, the command produces no output and returns you to the prompt. You should also see a new dist/contactBook.js file appear.

Seeing an import error?

Make sure the import path ends with .js and not .ts. With nodenext module resolution, TypeScript expects the compiled output extension in import paths.

Also confirm that src/types.ts exists and exports both Contact and ContactCategory.

Help me fix my TypeScript import error in contactBook.ts

Implement addContact and listContacts

Your class needs a way to create new contacts and retrieve the full list. The addContact method takes typed parameters and returns the newly created contact. The listContacts method returns the entire array.

  • In src/contactBook.ts, add the following two methods inside the ContactBook class, below the private nextId line:
  // Create a new contact with a unique ID and add it to the collection
  addContact(
    name: string,
    phone: string,
    category: ContactCategory,
    email?: string
  ): Contact {
    const newContact: Contact = {
      id: this.nextId++,
      name,
      phone,
      email,
      category,
    };
    this.contacts.push(newContact);
    return newContact;
  }

  // Return all contacts in the collection
  listContacts(): Contact[] {
    return this.contacts;
  }

What does this code do?

  • addContact takes four parameters. The email? parameter is optional (marked with ?), matching your Contact interface.
  • this.nextId++ assigns the current ID to the new contact and then increments the counter for the next one.
  • The return type : Contact guarantees the method always returns a properly shaped contact object. TypeScript would flag an error if you forgot a required field.
  • listContacts returns Contact[]. Anyone calling this method knows exactly what shape of data they'll get back.
  • Save src/contactBook.ts.

You won't compile yet. The next two methods complete the class, and you'll verify everything compiles together.

Implement findByName and deleteContact

A contact book isn't useful if you can't search it or remove entries. These two methods use typed array methods (.filter() and .findIndex()) with fully typed callback parameters.

  • Below the listContacts method in src/contactBook.ts, add the findByName method:
  // Search contacts by name (case-insensitive partial match)
  findByName(searchTerm: string): Contact[] {
    return this.contacts.filter((contact) =>
      contact.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }

What does this code do?

The .filter() method returns a new array containing only contacts whose name includes the search term. Because contact is typed as Contact, TypeScript knows contact.name is a string. If you accidentally typed contact.nme, the compiler would catch the typo immediately.

  • Save src/contactBook.ts.

One more method to go. The deleteContact method completes your CRUD operations.

  • Below the findByName method, add the deleteContact method:
  // Remove a contact by ID, return true if found and deleted
  deleteContact(id: number): boolean {
    const index = this.contacts.findIndex((contact) => contact.id === id);
    if (index === -1) {
      return false;
    }
    this.contacts.splice(index, 1);
    return true;
  }

What does this code do?

  • .findIndex() locates the position of the contact with the matching id. It returns -1 if no match exists.
  • The return type : boolean makes the method's contract explicit. Callers know they'll get true (deleted) or false (not found) without reading the implementation.
  • .splice(index, 1) removes exactly one element at the found position.
  • Save src/contactBook.ts.
  • Compile the full project by running:
npx tsc

No output means success. TypeScript verified every type annotation, method signature, and return type in your class without finding a single error.

Seeing a compiler error?

Check that your methods are inside the class body (between the opening { after export class ContactBook and the final closing }). A method placed outside the class braces causes a syntax error.

Also verify that email?: string in addContact uses a question mark. Without it, TypeScript treats email as required and the type won't match your interface.

Help me fix my TypeScript compiler error in contactBook.ts

✔️ Awesome, I've got everything!

Great. Double check you've saved src/contactBook.ts and the compile produced no errors.

ⓧ I'd like to double check the full code

Here's the complete src/contactBook.ts file for comparison:

import { Contact, ContactCategory } from "./types.js";

export class ContactBook {
  private contacts: Contact[] = [];
  private nextId: number = 1;

  addContact(
    name: string,
    phone: string,
    category: ContactCategory,
    email?: string
  ): Contact {
    const newContact: Contact = {
      id: this.nextId++,
      name,
      phone,
      email,
      category,
    };
    this.contacts.push(newContact);
    return newContact;
  }

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

  findByName(searchTerm: string): Contact[] {
    return this.contacts.filter((contact) =>
      contact.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }

  deleteContact(id: number): boolean {
    const index = this.contacts.findIndex((contact) => contact.id === id);
    if (index === -1) {
      return false;
    }
    this.contacts.splice(index, 1);
    return true;
  }
}

Your ContactBook class is complete with four fully typed methods. In the next step, you'll wire it into your main script and see it in action.

Wire It All Together and See TypeScript in Action

In the previous step, you built a ContactBook class with type-safe methods for adding, listing, searching, and deleting contacts.

Now you'll put it all together by writing a main script that exercises every feature. You'll also intentionally break your code to watch TypeScript catch bugs at compile time that JavaScript would only reveal at runtime.

In this step, get ready to:
  • Use the ContactBook class to add, list, search, and delete contacts.
  • Introduce intentional type errors and observe TypeScript catching them.
  • Compile and run your completed Contact Book CLI.
Use the ContactBook class in your main script

Your src/index.ts currently holds a sample contact object from the previous step. You'll replace all of its contents with code that imports and uses your ContactBook class.

  • In src/index.ts, select all the existing code and delete it.
  • Replace it with the following code that imports the ContactBook and adds three contacts:
import { ContactBook } from "./contactBook.js";

const book = new ContactBook();

// Add contacts
const alice = book.addContact("Alice Johnson", "555-0101", "work", "alice@company.com");
const bob = book.addContact("Bob Smith", "555-0102", "personal");
const carol = book.addContact("Carol Williams", "555-0103", "family", "carol@email.com");

console.log("=== Contact Book ===\n");

What does this code do?

  • The import pulls in your ContactBook class from ./contactBook.js. The .js extension is required by Node.js ESM module resolution, even though the source file is .ts.
  • Each addContact call returns the created Contact object, stored in a variable so you can reference it later (like alice.id for deletion).
  • Notice that bob has no email argument. TypeScript allows this because email is defined as optional (email?: string) in the Contact interface.
  • Now add the following code directly below the console.log("=== Contact Book ===\n"); line to list, search, delete, and re-list contacts:
// List all contacts
console.log("All contacts:");
book.listContacts().forEach((contact) => {
  const emailInfo = contact.email ? ` (${contact.email})` : "";
  console.log(`  [${contact.category}] ${contact.name} - ${contact.phone}${emailInfo}`);
});

// Search
console.log("\nSearch for 'bob':");
const results = book.findByName("bob");
results.forEach((contact) => {
  console.log(`  Found: ${contact.name} - ${contact.phone}`);
});

// Delete
console.log(`\nDeleting contact with id ${alice.id}...`);
const deleted = book.deleteContact(alice.id);
console.log(`  Deleted: ${deleted}`);

// List again
console.log("\nRemaining contacts:");
book.listContacts().forEach((contact) => {
  console.log(`  ${contact.name} - ${contact.phone}`);
});

What does this code do?

  • The listing block calls listContacts() and formats each contact with its category, name, phone, and optional email. TypeScript knows contact.email might be undefined, so the ternary check is required for safe access.
  • The search uses findByName("bob") and TypeScript ensures you pass a string argument.
  • The delete uses alice.id which TypeScript knows is a number because addContact returns a typed Contact object.
  • Save src/index.ts.

Seeing red squiggly lines in your editor?

If your editor shows errors on the import line, make sure you saved src/contactBook.ts in the previous step. The import path ./contactBook.js must match the filename exactly (case-sensitive).

help me fix import errors in my TypeScript file

✔️ Awesome, I've got everything!

Great. Double check you've saved src/index.ts before moving on.

ⓧ I'd like to double check the full code

Here is the complete src/index.ts file at this point:

import { ContactBook } from "./contactBook.js";

const book = new ContactBook();

// Add contacts
const alice = book.addContact("Alice Johnson", "555-0101", "work", "alice@company.com");
const bob = book.addContact("Bob Smith", "555-0102", "personal");
const carol = book.addContact("Carol Williams", "555-0103", "family", "carol@email.com");

console.log("=== Contact Book ===\n");

// List all contacts
console.log("All contacts:");
book.listContacts().forEach((contact) => {
  const emailInfo = contact.email ? ` (${contact.email})` : "";
  console.log(`  [${contact.category}] ${contact.name} - ${contact.phone}${emailInfo}`);
});

// Search
console.log("\nSearch for 'bob':");
const results = book.findByName("bob");
results.forEach((contact) => {
  console.log(`  Found: ${contact.name} - ${contact.phone}`);
});

// Delete
console.log(`\nDeleting contact with id ${alice.id}...`);
const deleted = book.deleteContact(alice.id);
console.log(`  Deleted: ${deleted}`);

// List again
console.log("\nRemaining contacts:");
book.listContacts().forEach((contact) => {
  console.log(`  ${contact.name} - ${contact.phone}`);
});
Break your code on purpose to see TypeScript's safety net

Before you compile, let's see what happens when you write code that TypeScript considers invalid. This is where TypeScript's value really shines.

  • In src/index.ts, find this line:
const bob = book.addContact("Bob Smith", "555-0102", "personal");
  • Replace it with this version that has three intentional errors:
const bob = book.addContact("Bob Smith", 555, "business");
console.log(alice.address);

What errors did you introduce?

  • Error 1: 555 is a number, but addContact expects phone to be a string.
  • Error 2: "business" is not a valid ContactCategory. The union type only allows "personal", "work", or "family".
  • Error 3: alice.address accesses a property that does not exist on the Contact interface.
  • Try to compile by running:
npx tsc

TypeScript refuses to compile and prints error messages explaining exactly what went wrong and where. Each error points to the file, line number, and the type mismatch.

Why does this matter?

In plain JavaScript, all three of these mistakes would silently succeed. You'd only discover the bug at runtime when your app behaves unexpectedly (or crashes). TypeScript catches them before a single line of your code runs.

Fix the errors and run your Contact Book
  • In src/index.ts, find the broken lines you just added:
const bob = book.addContact("Bob Smith", 555, "business");
console.log(alice.address);
  • Delete console.log(alice.address); entirely, and replace the bob line with the correct version:
const bob = book.addContact("Bob Smith", "555-0102", "personal");
  • Save src/index.ts.
  • Compile the project by running:
npx tsc

This time the compiler finishes without any errors. Your compiled JavaScript files are in the dist/ folder.

  • Run your Contact Book by executing:
node dist/index.js

You should see output that lists all three contacts with their categories, shows the search result for Bob, confirms the deletion of Alice, and lists the remaining two contacts.

Not seeing any output?

Make sure npx tsc completed without errors first. If it printed errors, fix them before running node dist/index.js. The dist/ folder only updates when compilation succeeds.

Also verify your package.json has "type": "module". Without it, Node.js cannot resolve ESM imports.

help me debug why my Contact Book isn't producing output

Your Contact Book CLI is fully functional. You've seen TypeScript catch type mismatches, invalid union values, and non-existent properties before your code ever runs. Up next, take on the Secret Mission to add generics and category filtering.

Secret mission

Add Favorites with Generics and Category Filtering

Your Contact Book works great, but what if you could mark certain contacts as favorites and filter by category? In this challenge, you'll use TypeScript generics to build a reusable filter utility that works with full type safety.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files for reference or delete them to free up disk space.

Resources you used:

  • The contact-book project folder on your Desktop (includes source code, compiled output in dist/, and node_modules).
  • Node.js (installed as a system-level runtime).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with TypeScript or want to reference this project later.

  • Your contact-book folder stays on your Desktop with all source files and compiled output intact.
  • Node.js remains installed and available for other JavaScript or TypeScript projects.

Pause - I'll come back to this later

Since this is a local project with no running processes, there is nothing to pause. You can free up disk space by removing the dependencies folder while keeping your source code.

  • Delete the node_modules folder to reclaim space. Run this from inside your contact-book folder:

macOS

rm -rf node_modules

Windows

Remove-Item -Recurse -Force node_modules
  • When you come back, run npm install to restore your dependencies. Then run npx tsc and node dist/index.js to pick up where you left off.

Delete - I don't want to use this again

Remove all project files from your Desktop. Optionally uninstall Node.js if you no longer need it for other projects.

  • Delete the entire project folder by running this command:

macOS

cd ~/Desktop
rm -rf contact-book

Windows

cd ~\Desktop
Remove-Item -Recurse -Force contact-book
  • To uninstall Node.js on Windows: open Settings, select Apps, then Installed apps. Find Node.js in the list and click Uninstall.
  • To uninstall Node.js on macOS (if you installed via Homebrew):
brew uninstall node
  • If you installed Node.js using the official installer or a version manager like nvm, follow the uninstall instructions for that specific tool.

Nice Work!

Nice Work!

You just built a fully typed Contact Book CLI in TypeScript from scratch. Your app manages contacts with complete compile-time type safety, and you've seen firsthand how TypeScript catches bugs before your code ever runs.

You've learned how to:

  • Set up a TypeScript project with tsconfig.json and strict mode, configuring the compiler to catch errors early.
  • Defined interfaces and union types to model your contact data with compile-time guarantees.
  • Built a type-safe ContactBook class with CRUD operations and compiled it to JavaScript you can run with Node.js.
  • Added generics for reusable, type-safe utility functions that filter contacts by favorites and category.

Ready to quiz yourself?