Are you a developer who loves the command line? Do you find yourself juggling multiple tasks and projects, wishing for a simple way to stay organized? In this tutorial, we’ll dive into the world of TypeScript and build a straightforward command-line task manager. This project will not only help you manage your daily tasks more efficiently but also provide a hands-on learning experience in TypeScript, covering essential concepts like types, interfaces, and modules. We’ll keep it simple, focusing on core functionality to get you up and running quickly.
Why Build a Command-Line Task Manager?
Command-line tools offer a unique blend of efficiency and control. They allow you to interact with your system directly, automating repetitive tasks and streamlining your workflow. A task manager, in particular, can be a lifesaver for developers, helping you keep track of what needs to be done, prioritize tasks, and avoid the mental overhead of remembering everything. Building one yourself gives you complete control over its features and how it works, tailored to your specific needs.
Setting Up Your Environment
Before we start coding, let’s set up our development environment. 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, create a new project directory and initialize a Node.js project using npm:
mkdir task-manager-cli
cd task-manager-cli
npm init -y
This will create a package.json file in your project directory. Next, let’s install TypeScript and the necessary type definitions for Node.js:
npm install typescript @types/node --save-dev
We’re also going to use a library called commander to handle command-line arguments. Install it like this:
npm install commander
Finally, create a tsconfig.json file in your project directory. This file configures the TypeScript compiler. Here’s a basic configuration:
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
This configuration tells the compiler to target ECMAScript 2016, use CommonJS modules, output the compiled JavaScript to a dist directory, and enable various strict checks.
Creating the Task Interface and Data Model
Let’s start by defining an interface for our tasks. Create a new directory called src and a file named src/Task.ts. Inside this file, we’ll define the Task interface:
// src/Task.ts
export interface Task {
id: number;
description: string;
completed: boolean;
}
This interface defines the structure of a task: an id (a number), a description (a string), and a completed status (a boolean). Next, we’ll create a simple data model to store our tasks. Create a file named src/tasks.ts:
// src/tasks.ts
import { Task } from './Task';
let tasks: Task[] = [];
export function getTasks(): Task[] {
return tasks;
}
export function addTask(description: string): void {
const newTask: Task = {
id: Date.now(), // Simple ID generation
description,
completed: false,
};
tasks.push(newTask);
console.log(`Task added: ${newTask.description}`);
}
export function completeTask(id: number): void {
const taskIndex = tasks.findIndex((task) => task.id === id);
if (taskIndex !== -1) {
tasks[taskIndex].completed = true;
console.log(`Task completed: ${tasks[taskIndex].description}`);
} else {
console.log("Task not found.");
}
}
export function deleteTask(id: number): void {
const taskIndex = tasks.findIndex((task) => task.id === id);
if (taskIndex !== -1) {
const deletedTask = tasks.splice(taskIndex, 1)[0];
console.log(`Task deleted: ${deletedTask.description}`);
} else {
console.log("Task not found.");
}
}
This file exports functions to add, complete, delete, and retrieve tasks. It uses an in-memory array (tasks) to store the tasks. In a real-world application, you would likely use a database or local storage.
Building the Command-Line Interface with Commander
Now, let’s create the main entry point for our command-line application. Create a file named src/index.ts:
// src/index.ts
import { program } from 'commander';
import { addTask, getTasks, completeTask, deleteTask } from './tasks';
program
.name('task-manager')
.description('A simple command-line task manager')
.version('1.0.0');
program
.command('add ')
.description('Add a new task')
.action((description: string) => {
addTask(description);
});
program
.command('list')
.description('List all tasks')
.action(() => {
const tasks = getTasks();
if (tasks.length === 0) {
console.log('No tasks yet.');
} else {
tasks.forEach((task) => {
console.log(`${task.id}: ${task.description} - ${task.completed ? 'Completed' : 'Pending'}`);
});
}
});
program
.command('complete ')
.description('Mark a task as completed')
.action((id: string) => {
completeTask(parseInt(id, 10));
});
program
.command('delete ')
.description('Delete a task')
.action((id: string) => {
deleteTask(parseInt(id, 10));
});
program.parse(process.argv);
This file uses the commander library to define commands and their associated actions. We define four commands: add, list, complete, and delete. Each command takes arguments and performs actions based on those arguments. For example, the add command takes a description as an argument and calls the addTask function from tasks.ts. The complete and delete commands take an ID as an argument. The list command displays all tasks.
Compiling and Running the Application
Now that we’ve written our code, let’s compile it and run it. Open your terminal and run the following command:
tsc
This command compiles your TypeScript code into JavaScript and places the output in the dist directory. To run your application, use the following command:
node dist/index.js add "Buy groceries"
This will add a task with the description “Buy groceries.” You can then list the tasks using:
node dist/index.js list
You can mark a task as complete using:
node dist/index.js complete 1 # Replace 1 with the task ID
And delete a task using:
node dist/index.js delete 1 # Replace 1 with the task ID
Handling Errors and Edge Cases
Our current implementation is functional, but it could be improved by handling errors and edge cases. For example, what happens if the user provides an invalid ID to the complete or delete commands? Let’s add some error handling to our completeTask and deleteTask functions in src/tasks.ts:
// src/tasks.ts (updated)
// ... (previous code)
export function completeTask(id: number): void {
const taskIndex = tasks.findIndex((task) => task.id === id);
if (taskIndex !== -1) {
tasks[taskIndex].completed = true;
console.log(`Task completed: ${tasks[taskIndex].description}`);
} else {
console.error("Error: Task not found."); // Use console.error for errors
}
}
export function deleteTask(id: number): void {
const taskIndex = tasks.findIndex((task) => task.id === id);
if (taskIndex !== -1) {
const deletedTask = tasks.splice(taskIndex, 1)[0];
console.log(`Task deleted: ${deletedTask.description}`);
} else {
console.error("Error: Task not found."); // Use console.error for errors
}
}
We’ve changed console.log to console.error when a task is not found. This will make the error messages more visible. You can also add more robust error handling, such as validating user input and providing informative error messages.
Adding Features: Task Prioritization
Let’s add a new feature: task prioritization. First, we’ll modify the Task interface to include a priority property. Update src/Task.ts:
// src/Task.ts (updated)
export enum Priority {
High = 'high',
Medium = 'medium',
Low = 'low',
}
export interface Task {
id: number;
description: string;
completed: boolean;
priority: Priority;
}
We’ve added an enum Priority with three possible values: “high”, “medium”, and “low”. We’ve also added a priority property of type Priority to the Task interface. Now, we need to modify the addTask function in src/tasks.ts to accept a priority and set a default priority if none is provided:
// src/tasks.ts (updated)
import { Task, Priority } from './Task';
// ... (previous code)
export function addTask(description: string, priority: Priority = Priority.Medium): void {
const newTask: Task = {
id: Date.now(),
description,
completed: false,
priority,
};
tasks.push(newTask);
console.log(`Task added: ${newTask.description} (Priority: ${priority})`);
}
We’ve added a priority parameter to the addTask function with a default value of Priority.Medium. Finally, we need to update the command definition in src/index.ts to accept a priority argument:
// src/index.ts (updated)
import { program } from 'commander';
import { addTask, getTasks, completeTask, deleteTask } from './tasks';
import { Priority } from './Task';
program
// ... (previous code)
program
.command('add ')
.description('Add a new task')
.option('-p, --priority ', 'Task priority (high, medium, low)', 'medium')
.action((description: string, options) => {
const priority = options.priority as Priority;
addTask(description, priority);
});
program
// ... (previous code)
We’ve added an option -p or --priority to the add command. The option takes a string argument. We also updated the action to extract the priority from the options and pass it to the addTask function. Now, you can add tasks with a specified priority like this:
node dist/index.js add "Prepare presentation" -p high
Or use the long form:
node dist/index.js add "Prepare presentation" --priority low
Common Mistakes and How to Fix Them
When working with TypeScript and command-line applications, here are some common mistakes and how to fix them:
- Incorrect File Paths: Make sure your file paths in import statements are correct. TypeScript can be strict about this. Use relative paths (e.g.,
./Task) to import modules from the same directory or subdirectories. - Type Errors: TypeScript’s type system can be a lifesaver, but it can also be frustrating if you’re not used to it. Pay close attention to type errors in your editor or during compilation. Use type annotations (e.g.,
let myVariable: string) to specify the expected types of variables and function parameters. - Incorrect Command-Line Arguments: Double-check the order and format of your command-line arguments. The
commanderlibrary is sensitive to how you define your commands and options. - Forgetting to Compile: Always remember to compile your TypeScript code (
tsc) before running your application. If you make changes to your TypeScript files, you need to recompile them to generate updated JavaScript files. - Incorrect Module Imports: Ensure you are importing modules correctly. If you’re importing a module, make sure you’ve installed it using npm and that the import statement matches the module’s export.
Key Takeaways and Summary
In this tutorial, we’ve built a simple command-line task manager using TypeScript. We’ve covered the basics of setting up a TypeScript project, defining interfaces and data models, using the commander library to handle command-line arguments, and adding features like task prioritization. You’ve learned how to structure a TypeScript project, handle user input, and build a functional command-line application.
Here’s a summary of what we’ve covered:
- We created a basic task manager application.
- We used TypeScript to define types and interfaces for better code organization and readability.
- We used the commander library to parse command-line arguments.
- We added features like adding, listing, completing, and deleting tasks.
- We added task prioritization.
- We discussed common mistakes and how to avoid them.
FAQ
Here are some frequently asked questions about building a command-line task manager in TypeScript:
- Can I store tasks in a database instead of an in-memory array? Yes, you can. You would replace the in-memory array with code that interacts with a database (e.g., SQLite, PostgreSQL, MongoDB) or uses local storage. You’ll need to install the appropriate database client library (e.g.,
sqlite3for SQLite) and modify theaddTask,getTasks,completeTask, anddeleteTaskfunctions to interact with the database. - How can I add more advanced features, such as due dates and recurring tasks? To add more advanced features, you’ll need to modify the
Taskinterface to include properties for due dates, recurrence rules, etc. You’ll also need to update the command definitions and actions to handle these new properties. You might also want to use a library for date and time manipulation (e.g.,date-fns) and a library for scheduling tasks. - How can I add a configuration file to store settings? You can use a library like
iniorconfigstoreto read and write configuration files. You would define a configuration schema (e.g., a file path for storing tasks, a default priority) and load the configuration when the application starts. The command-line interface could also be extended to allow the user to modify the configuration. - How can I add tests to this application? You can use a testing framework like Jest or Mocha to write unit tests for your functions (e.g.,
addTask,completeTask). You would create test files that import the functions you want to test and write assertions to check their behavior. For example, you could test thataddTaskadds a task to the task list correctly.
Now, you have a foundation to build upon. This simple command-line task manager can be extended to include more features, better error handling, and more sophisticated data storage solutions. You can integrate this tool into your daily workflow, customizing it to meet your specific needs. The core concepts of TypeScript, such as types, interfaces, and module organization, will be invaluable as you build more complex applications. With a solid understanding of these fundamentals, you are well-equipped to tackle more complex projects and continue your journey in the world of TypeScript development.
