TypeScript Tutorial: Creating a Simple Command-Line Task Manager

In the world of software development, managing tasks effectively is paramount. Whether you’re a seasoned developer or just starting out, keeping track of what needs to be done, prioritizing tasks, and ensuring you meet deadlines is crucial. While numerous graphical task management tools exist, sometimes you need something simpler, faster, and more integrated into your development workflow. This is where a command-line task manager built with TypeScript comes in handy. This tutorial will guide you through creating a basic, yet functional, command-line task manager, empowering you to manage your tasks directly from your terminal.

Why Build a Command-Line Task Manager?

Command-line tools offer several advantages for developers:

  • Efficiency: They can be quicker to access and use than graphical interfaces, especially if you’re already working in the terminal.
  • Automation: Easily integrated into scripts and automated workflows.
  • Customization: Tailored to your specific needs and preferences.
  • Learning: Building such a tool is a great way to learn and practice TypeScript concepts.

This tutorial focuses on creating a task manager that allows you to add, list, mark as complete, and delete tasks. We’ll be using TypeScript to ensure type safety and code maintainability.

Prerequisites

Before we begin, make sure you have the following installed:

  • Node.js and npm (Node Package Manager): Required for running TypeScript and managing dependencies. Download from nodejs.org.
  • TypeScript: Installed globally using npm: npm install -g typescript
  • A Code Editor: Such as VS Code, Sublime Text, or Atom.
  • Basic familiarity with JavaScript and the command line.

Setting Up Your Project

Let’s start by setting up our project directory and initializing it.

  1. Create a new directory for your project: mkdir task-manager-cli
  2. Navigate into the directory: cd task-manager-cli
  3. Initialize a Node.js project: npm init -y (This creates a package.json file.)
  4. Install TypeScript as a dev dependency: npm install --save-dev typescript @types/node
  5. Create a tsconfig.json file: tsc --init (This sets up your TypeScript configuration.)

Your directory structure should now look something like this:

task-manager-cli/
├── node_modules/
├── package.json
├── package-lock.json
├── tsconfig.json
└──

Modify your tsconfig.json file to match the following for basic configuration:

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

This configuration tells TypeScript to:

  • Compile to ES2016.
  • Use CommonJS module system.
  • Output compiled files to a dist directory.
  • Start looking for files in the src directory.
  • Enable strict type checking.

Creating the Core Task Manager Logic

Create a directory named src and inside it, create a file named index.ts. This is where we’ll write the main logic of our task manager.

Open src/index.ts and let’s start with defining a Task interface and an array to hold our tasks:

// src/index.ts

interface Task {
  id: number;
  description: string;
  completed: boolean;
}

let tasks: Task[] = [];

Next, let’s create functions to add, list, mark as complete, and delete tasks. We’ll also add a helper function to read task data from a file so our tasks persist beyond a single session (more on this later).

// src/index.ts
import * as fs from 'fs';
import * as path from 'path';

interface Task {
  id: number;
  description: string;
  completed: boolean;
}

let tasks: Task[] = [];
const dataFilePath = path.join(__dirname, 'tasks.json');

// Helper function to load tasks from file
function loadTasks(): void {
  try {
    const data = fs.readFileSync(dataFilePath, 'utf-8');
    tasks = JSON.parse(data) as Task[];
  } catch (error) {
    // If the file doesn't exist or there's an error, start with an empty array
    tasks = [];
  }
}

// Helper function to save tasks to file
function saveTasks(): void {
  fs.writeFileSync(dataFilePath, JSON.stringify(tasks, null, 2), 'utf-8');
}


function addTask(description: string): void {
  const newTask: Task = {
    id: Date.now(), // Simple ID generation
    description,
    completed: false,
  };
  tasks.push(newTask);
  saveTasks(); // Save tasks after adding
  console.log(`Task added: ${newTask.description}`);
}

function listTasks(): void {
  if (tasks.length === 0) {
    console.log('No tasks yet.');
    return;
  }

  tasks.forEach((task) => {
    const status = task.completed ? '[x]' : '[ ]';
    console.log(`${task.id} ${status} ${task.description}`);
  });
}

function completeTask(id: number): void {
  const taskIndex = tasks.findIndex((task) => task.id === id);

  if (taskIndex === -1) {
    console.log(`Task with ID ${id} not found.`);
    return;
  }

  tasks[taskIndex].completed = true;
  saveTasks(); // Save tasks after completing
  console.log(`Task ${id} marked as complete.`);
}

function deleteTask(id: number): void {
  tasks = tasks.filter((task) => task.id !== id);
  saveTasks(); // Save tasks after deleting
  console.log(`Task ${id} deleted.`);
}

// Load tasks when the program starts
loadTasks();

Explanation of the Code:

  • Task Interface: Defines the structure of a task (id, description, completed).
  • tasks Array: Stores the tasks.
  • addTask(description: string): Adds a new task to the array. It generates a simple ID using Date.now().
  • listTasks(): Displays all tasks, with their status (completed or not).
  • completeTask(id: number): Marks a task as complete based on its ID.
  • deleteTask(id: number): Removes a task based on its ID.
  • loadTasks(): Reads tasks from a JSON file to persist data.
  • saveTasks(): Writes tasks to a JSON file.

Implementing Command-Line Arguments

Now, let’s enable our command-line application to accept arguments. We’ll use the process.argv array to parse the arguments passed to our script.

// src/index.ts (continued)

// ... (previous code)

function main(): void {
  const args = process.argv.slice(2); // Remove 'node' and the script name
  const command = args[0];

  switch (command) {
    case 'add':
      const description = args.slice(1).join(' '); // Join the rest of the arguments as the description
      if (!description) {
        console.log('Please provide a task description.');
        break;
      }
      addTask(description);
      break;
    case 'list':
      listTasks();
      break;
    case 'complete':
      const completeId = parseInt(args[1], 10);
      if (isNaN(completeId)) {
        console.log('Please provide a valid task ID to complete.');
        break;
      }
      completeTask(completeId);
      break;
    case 'delete':
      const deleteId = parseInt(args[1], 10);
      if (isNaN(deleteId)) {
        console.log('Please provide a valid task ID to delete.');
        break;
      }
      deleteTask(deleteId);
      break;
    case 'help':
      console.log(
        'Usage: task-manager-cli  [options]'
      );
      console.log('Commands:');
      console.log('  add   Adds a new task.');
      console.log('  list                     Lists all tasks.');
      console.log('  complete        Marks a task as complete.');
      console.log('  delete          Deletes a task.');
      console.log('  help                     Displays this help message.');
      break;
    default:
      console.log('Invalid command. Use `help` for usage information.');
  }
}

main();

Explanation of the Code:

  • process.argv: An array containing the command-line arguments. The first two elements are always ‘node’ and the script’s file path; we slice the array from index 2 to get the actual arguments.
  • command: The first argument is treated as the command (e.g., ‘add’, ‘list’).
  • switch statement: Handles different commands and calls the appropriate functions.
  • Argument Parsing: Extracts necessary information (task description, task ID) from the arguments.
  • Error Handling: Includes basic error checking (e.g., checking if a description is provided for ‘add’, or if the ID is a number for ‘complete’ and ‘delete’).

Compiling and Running Your Task Manager

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

  1. Compile the code: In your terminal, run tsc. This will compile the TypeScript files in the src directory and output JavaScript files in the dist directory.
  2. Run the task manager: You can execute your task manager using Node.js. For example:
node dist/index.js list

Or, to add a task:

node dist/index.js add "Grocery shopping"

To complete a task (assuming its ID is 123):

node dist/index.js complete 123

To delete a task (assuming its ID is 123):

node dist/index.js delete 123

Enhancements and Further Development

This is a basic task manager, but you can extend it with several features:

  • User Interface: Use a library like inquirer to create an interactive command-line interface with prompts and menus.
  • Prioritization: Add a priority level to tasks (e.g., high, medium, low).
  • Due Dates: Allow users to set due dates for tasks.
  • Categories/Tags: Organize tasks by categories or tags.
  • Persistence: Store tasks in a database (e.g., using SQLite or a cloud database). The current implementation uses a simple JSON file, but a database offers more robust storage.
  • Error Handling: Implement more comprehensive error handling, logging, and user feedback.
  • Testing: Write unit tests to ensure the code functions correctly.
  • Configuration: Allow users to configure the task manager (e.g., where to store data).
  • Color Coding: Use libraries like chalk to add color to the output for better readability.

Common Mistakes and Troubleshooting

Here are some common mistakes and how to fix them:

  • Typo Errors: TypeScript helps prevent typos, but double-check your code for any spelling errors, especially in variable and function names. Use your editor’s auto-completion features to minimize these errors.
  • Incorrect Paths: When working with files (like our tasks.json), ensure that the file paths are correct. Use the path.join() method to construct paths to avoid platform-specific issues.
  • Incorrect Argument Parsing: Carefully parse the command-line arguments to avoid unexpected behavior. Make sure you are correctly extracting the command and any required options.
  • Missing Dependencies: Ensure you have installed all necessary dependencies using npm. If you are using a library, always check the documentation for specific installation instructions.
  • Incorrect File Permissions: Ensure that the application has the necessary permissions to read and write to the task data file.
  • Type Errors: TypeScript’s type checking can be very helpful. Carefully review any type errors reported by the compiler and fix them.
  • Incorrect Imports: Make sure you import modules correctly. For example, if you are using a module from the standard library like fs, ensure you are importing it correctly with import * as fs from 'fs';

Key Takeaways

  • TypeScript provides strong typing, making your code more robust and maintainable.
  • Command-line tools can be efficient and easily integrated into development workflows.
  • Understanding process.argv is crucial for creating command-line applications.
  • File I/O is necessary for persisting data.
  • This tutorial provides a solid foundation for building more complex command-line applications.

FAQ

Here are some frequently asked questions:

  1. How do I install TypeScript?

    You install TypeScript globally using npm: npm install -g typescript. You’ll also need to install the TypeScript compiler in your project using npm install --save-dev typescript.

  2. How do I compile my TypeScript code?

    Use the command tsc in your terminal. This will compile all TypeScript files in your src directory and output JavaScript files in the dist directory (based on your tsconfig.json configuration).

  3. How do I run my TypeScript application?

    First, compile your TypeScript code using tsc. Then, run the generated JavaScript file using Node.js: node dist/index.js <command> <options> (e.g., node dist/index.js add "Buy groceries").

  4. How can I handle errors in my task manager?

    You can add try...catch blocks around potentially error-prone operations (like file I/O). You can also add checks for invalid user input and provide informative error messages.

  5. Can I use a database instead of a JSON file?

    Yes, using a database (like SQLite, PostgreSQL, or MongoDB) is a good choice for more complex applications. It offers better scalability, data integrity, and features like indexing and querying.

Building a command-line task manager is a fantastic exercise for understanding the basics of TypeScript, command-line argument parsing, and file I/O. It provides a practical application for these concepts, allowing you to create a tool that can streamline your daily workflow. Remember to break the problem into smaller, manageable parts, test your code frequently, and don’t be afraid to experiment with different features and enhancements. The journey of building software is as much about the learning process as it is about the final product. So, keep coding, keep learning, and enjoy the process of bringing your ideas to life.