Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on building CLI tools with Commander.js and TypeScript! ๐ In this guide, weโll explore how to create powerful command-line interfaces that are both type-safe and user-friendly.
Youโll discover how Commander.js can transform your TypeScript development experience. Whether youโre building developer tools ๐ ๏ธ, automation scripts ๐ค, or interactive utilities ๐ฎ, understanding CLI development is essential for creating professional command-line applications.
By the end of this tutorial, youโll feel confident building your own CLI tools with TypeScript! Letโs dive in! ๐โโ๏ธ
๐ Understanding CLI Tools and Commander.js
๐ค What is Commander.js?
Commander.js is like a friendly assistant for building command-line interfaces ๐จ. Think of it as a Swiss Army knife ๐ง that helps you create professional CLI tools with minimal effort.
In TypeScript terms, Commander.js provides a declarative API for defining commands, options, and arguments with full type safety. This means you can:
- โจ Create intuitive command interfaces
- ๐ Parse arguments automatically
- ๐ก๏ธ Validate inputs with TypeScript types
๐ก Why Use Commander.js with TypeScript?
Hereโs why developers love this combination:
- Type Safety ๐: Catch command errors at compile-time
- Better IDE Support ๐ป: Autocomplete for commands and options
- Code Documentation ๐: Types serve as inline docs
- Refactoring Confidence ๐ง: Change commands without fear
Real-world example: Imagine building a task manager CLI ๐. With Commander.js and TypeScript, you can ensure all commands have the right arguments and types!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
// ๐ Hello, CLI world!
import { Command } from 'commander';
// ๐จ Create a new program
const program = new Command();
// ๐ฏ Define basic program info
program
.name('my-cli')
.description('My awesome CLI tool! ๐')
.version('1.0.0');
// ๐ Add a simple command
program
.command('greet <name>')
.description('Greet someone with style! ๐')
.action((name: string) => {
console.log(`Hello ${name}! Welcome to TypeScript CLI development! ๐`);
});
// ๐ Parse the arguments
program.parse();
๐ก Explanation: Notice how we define commands declaratively! The <name>
syntax makes the argument required.
๐ฏ Common Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: Options with types
interface CliOptions {
debug?: boolean;
output?: string;
emoji?: boolean;
}
program
.option('-d, --debug', 'Enable debug mode ๐')
.option('-o, --output <file>', 'Output file path ๐')
.option('--no-emoji', 'Disable emojis ๐ข');
// ๐จ Pattern 2: Subcommands
program
.command('task <action>')
.description('Manage tasks ๐')
.option('-p, --priority <level>', 'Set priority (low|medium|high)')
.action((action: string, options: any) => {
console.log(`Task action: ${action} ๐ฏ`);
if (options.priority) {
console.log(`Priority: ${options.priority} โก`);
}
});
// ๐ Pattern 3: Interactive prompts
import { input, select } from '@inquirer/prompts';
program
.command('interactive')
.description('Start interactive mode ๐ฌ')
.action(async () => {
const name = await input({ message: 'What\'s your name? ๐ค' });
const color = await select({
message: 'Pick your favorite color ๐จ',
choices: [
{ value: 'red', name: '๐ด Red' },
{ value: 'blue', name: '๐ต Blue' },
{ value: 'green', name: '๐ข Green' }
]
});
console.log(`Hello ${name}! You picked ${color}! ๐`);
});
๐ก Practical Examples
๐ Example 1: Task Manager CLI
Letโs build something real:
// ๐๏ธ Define our task type
interface Task {
id: string;
title: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
emoji: string;
}
// ๐ Task storage (in-memory for demo)
class TaskManager {
private tasks: Task[] = [];
// โ Add a task
addTask(title: string, priority: Task['priority'] = 'medium'): void {
const task: Task = {
id: Date.now().toString(),
title,
completed: false,
priority,
emoji: this.getPriorityEmoji(priority)
};
this.tasks.push(task);
console.log(`โ
Added: ${task.emoji} ${task.title}`);
}
// ๐ List all tasks
listTasks(): void {
if (this.tasks.length === 0) {
console.log('๐ญ No tasks yet! Add one with "add" command');
return;
}
console.log('\n๐ Your Tasks:\n');
this.tasks.forEach((task, index) => {
const status = task.completed ? 'โ
' : 'โฌ';
console.log(`${index + 1}. ${status} ${task.emoji} ${task.title}`);
});
}
// โ
Complete a task
completeTask(index: number): void {
if (this.tasks[index]) {
this.tasks[index].completed = true;
console.log(`๐ Completed: ${this.tasks[index].title}`);
} else {
console.log('โ Task not found!');
}
}
// ๐จ Get emoji for priority
private getPriorityEmoji(priority: Task['priority']): string {
const emojis = {
low: '๐ข',
medium: '๐ก',
high: '๐ด'
};
return emojis[priority];
}
}
// ๐ฎ Create CLI
const taskManager = new TaskManager();
const program = new Command();
program
.name('tasks')
.description('Simple task manager CLI ๐')
.version('1.0.0');
// โ Add task command
program
.command('add <title>')
.description('Add a new task โ')
.option('-p, --priority <level>', 'Set priority: low, medium, high', 'medium')
.action((title: string, options: { priority: Task['priority'] }) => {
taskManager.addTask(title, options.priority);
});
// ๐ List tasks command
program
.command('list')
.description('List all tasks ๐')
.action(() => {
taskManager.listTasks();
});
// โ
Complete task command
program
.command('done <index>')
.description('Mark task as complete โ
')
.action((index: string) => {
taskManager.completeTask(parseInt(index) - 1);
});
program.parse();
๐ฏ Try it yourself: Add a remove
command and a filter option for listing!
๐ฎ Example 2: Project Generator CLI
Letโs make it fun:
// ๐ Project template system
interface ProjectTemplate {
name: string;
description: string;
emoji: string;
files: Map<string, string>;
}
class ProjectGenerator {
private templates: Map<string, ProjectTemplate> = new Map();
constructor() {
// ๐จ Initialize templates
this.templates.set('react', {
name: 'React App',
description: 'Modern React with TypeScript',
emoji: 'โ๏ธ',
files: new Map([
['src/App.tsx', '// ๐ Hello React!'],
['package.json', '{ "name": "my-react-app" }'],
['tsconfig.json', '{ "compilerOptions": {} }']
])
});
this.templates.set('api', {
name: 'REST API',
description: 'Express API with TypeScript',
emoji: '๐',
files: new Map([
['src/server.ts', '// ๐ฅ๏ธ Server setup'],
['package.json', '{ "name": "my-api" }'],
['.env', '# ๐ Environment variables']
])
});
}
// ๐ฏ Generate project
async generateProject(templateName: string, projectName: string): Promise<void> {
const template = this.templates.get(templateName);
if (!template) {
console.log('โ Template not found!');
return;
}
console.log(`\n${template.emoji} Creating ${template.name} project: ${projectName}\n`);
// ๐ Simulate file creation
for (const [file, content] of template.files) {
console.log(` โจ Creating ${file}`);
// In real app, use fs.writeFile here
}
console.log(`\n๐ Project ${projectName} created successfully!`);
console.log(`\n๐ Next steps:`);
console.log(` 1. cd ${projectName}`);
console.log(` 2. npm install`);
console.log(` 3. npm run dev`);
console.log(`\nHappy coding! ๐`);
}
// ๐ List templates
listTemplates(): void {
console.log('\n๐ฆ Available Templates:\n');
this.templates.forEach((template, key) => {
console.log(` ${template.emoji} ${key} - ${template.description}`);
});
}
}
// ๐ฎ CLI setup
const generator = new ProjectGenerator();
const program = new Command();
program
.name('create-app')
.description('Project generator CLI ๐๏ธ')
.version('1.0.0');
// ๐ Create command
program
.command('create <template> <name>')
.description('Create a new project ๐จ')
.action((template: string, name: string) => {
generator.generateProject(template, name);
});
// ๐ List templates
program
.command('templates')
.description('List available templates ๐ฆ')
.action(() => {
generator.listTemplates();
});
// ๐ฏ Interactive mode
program
.command('interactive')
.description('Interactive project creation ๐ฌ')
.action(async () => {
const template = await select({
message: 'Choose a template ๐ฆ',
choices: [
{ value: 'react', name: 'โ๏ธ React App' },
{ value: 'api', name: '๐ REST API' }
]
});
const name = await input({
message: 'Project name? ๐',
default: 'my-awesome-project'
});
await generator.generateProject(template, name);
});
program.parse();
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Custom Types and Validation
When youโre ready to level up, try this advanced pattern:
// ๐ฏ Advanced type-safe options
interface AdvancedOptions {
config?: string;
env?: 'dev' | 'prod' | 'test';
verbose?: boolean;
maxRetries?: number;
}
// ๐ช Custom type coercion
function parseEnv(value: string): 'dev' | 'prod' | 'test' {
const validEnvs = ['dev', 'prod', 'test'];
if (!validEnvs.includes(value)) {
throw new Error(`Invalid environment: ${value} ๐ข`);
}
return value as 'dev' | 'prod' | 'test';
}
// โจ Advanced command with validation
program
.command('deploy')
.description('Deploy your app ๐')
.option('-c, --config <path>', 'Config file path ๐')
.option('-e, --env <env>', 'Environment', parseEnv, 'dev')
.option('-v, --verbose', 'Verbose output ๐ข')
.option('--max-retries <n>', 'Max retry attempts', parseInt, 3)
.action((options: AdvancedOptions) => {
console.log('๐ Deploying with options:', options);
});
๐๏ธ Advanced Topic 2: Plugin System
For the brave developers:
// ๐ Plugin-based CLI architecture
interface CliPlugin {
name: string;
emoji: string;
register: (program: Command) => void;
}
class PluginManager {
private plugins: CliPlugin[] = [];
// ๐ฆ Register plugin
use(plugin: CliPlugin): void {
this.plugins.push(plugin);
console.log(`โจ Loaded plugin: ${plugin.emoji} ${plugin.name}`);
}
// ๐ฏ Apply all plugins
applyPlugins(program: Command): void {
this.plugins.forEach(plugin => {
plugin.register(program);
});
}
}
// ๐จ Example plugin
const gitPlugin: CliPlugin = {
name: 'Git Integration',
emoji: '๐',
register: (program) => {
program
.command('git:status')
.description('Show git status ๐')
.action(() => {
console.log('๐ Git status: All good! โ
');
});
}
};
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Missing Type Definitions
// โ Wrong way - no types for options!
program
.command('build')
.option('-m, --mode <mode>')
.action((options) => {
// options.mode is 'any' ๐ฐ
console.log(options.mode.toUpperCase()); // ๐ฅ Might crash!
});
// โ
Correct way - define option types!
interface BuildOptions {
mode?: 'development' | 'production';
}
program
.command('build')
.option('-m, --mode <mode>', 'Build mode')
.action((options: BuildOptions) => {
if (options.mode) {
console.log(`Building in ${options.mode} mode! ๐๏ธ`);
}
});
๐คฏ Pitfall 2: Forgetting Async Handling
// โ Dangerous - no error handling!
program
.command('fetch')
.action(() => {
fetch('https://api.example.com/data')
.then(res => res.json())
.then(data => console.log(data));
// ๐ฅ Program exits before fetch completes!
});
// โ
Safe - proper async handling!
program
.command('fetch')
.action(async () => {
try {
console.log('๐ Fetching data...');
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log('โ
Data received:', data);
} catch (error) {
console.error('โ Fetch failed:', error);
process.exit(1);
}
});
๐ ๏ธ Best Practices
- ๐ฏ Type Everything: Define interfaces for all options and arguments
- ๐ Clear Descriptions: Help users understand each command
- ๐ก๏ธ Validate Inputs: Use custom parsers for type safety
- ๐จ Consistent Naming: Use kebab-case for commands
- โจ Helpful Errors: Provide clear error messages with suggestions
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Password Manager CLI
Create a type-safe password manager CLI:
๐ Requirements:
- โ Add passwords with labels and optional tags
- ๐ Generate secure passwords with customizable options
- ๐ Search passwords by label or tag
- ๐ List all stored passwords (masked by default)
- ๐จ Each password entry needs a category emoji!
๐ Bonus Points:
- Add encryption for stored passwords
- Implement password strength checker
- Create export/import functionality
๐ก Solution
๐ Click to see solution
// ๐ฏ Our type-safe password manager!
interface Password {
id: string;
label: string;
password: string;
tags: string[];
category: 'work' | 'personal' | 'financial' | 'social';
createdAt: Date;
}
class PasswordManager {
private passwords: Map<string, Password> = new Map();
// โ Add a password
addPassword(label: string, password: string, category: Password['category'], tags: string[] = []): void {
const entry: Password = {
id: Date.now().toString(),
label,
password,
category,
tags,
createdAt: new Date()
};
this.passwords.set(entry.id, entry);
console.log(`โ
Added: ${this.getCategoryEmoji(category)} ${label}`);
}
// ๐ฒ Generate password
generatePassword(length: number = 16, options: { numbers?: boolean; symbols?: boolean } = {}): string {
let chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
if (options.numbers) chars += '0123456789';
if (options.symbols) chars += '!@#$%^&*()_+-=[]{}|;:,.<>?';
let password = '';
for (let i = 0; i < length; i++) {
password += chars[Math.floor(Math.random() * chars.length)];
}
console.log(`๐ฒ Generated password: ${password}`);
return password;
}
// ๐ List passwords
listPasswords(showPassword: boolean = false): void {
if (this.passwords.size === 0) {
console.log('๐ No passwords stored yet!');
return;
}
console.log('\n๐ Stored Passwords:\n');
this.passwords.forEach(entry => {
const masked = showPassword ? entry.password : 'โขโขโขโขโขโขโขโข';
console.log(`${this.getCategoryEmoji(entry.category)} ${entry.label}: ${masked}`);
if (entry.tags.length > 0) {
console.log(` ๐ท๏ธ Tags: ${entry.tags.join(', ')}`);
}
});
}
// ๐ Search passwords
searchPasswords(query: string): Password[] {
const results: Password[] = [];
this.passwords.forEach(entry => {
if (entry.label.includes(query) || entry.tags.some(tag => tag.includes(query))) {
results.push(entry);
}
});
return results;
}
// ๐จ Get category emoji
private getCategoryEmoji(category: Password['category']): string {
const emojis = {
work: '๐ผ',
personal: '๐ ',
financial: '๐ฐ',
social: '๐ฅ'
};
return emojis[category];
}
}
// ๐ฎ CLI Setup
const passwordManager = new PasswordManager();
const program = new Command();
program
.name('passkey')
.description('Secure password manager CLI ๐')
.version('1.0.0');
// โ Add password
program
.command('add <label>')
.description('Add a new password ๐')
.option('-p, --password <password>', 'Set password manually')
.option('-c, --category <category>', 'Category: work, personal, financial, social', 'personal')
.option('-t, --tags <tags...>', 'Add tags')
.action((label: string, options: any) => {
const password = options.password || passwordManager.generatePassword();
passwordManager.addPassword(label, password, options.category, options.tags || []);
});
// ๐ฒ Generate password
program
.command('generate')
.description('Generate secure password ๐ฒ')
.option('-l, --length <length>', 'Password length', parseInt, 16)
.option('-n, --numbers', 'Include numbers')
.option('-s, --symbols', 'Include symbols')
.action((options: any) => {
passwordManager.generatePassword(options.length, {
numbers: options.numbers,
symbols: options.symbols
});
});
// ๐ List passwords
program
.command('list')
.description('List all passwords ๐')
.option('-s, --show', 'Show actual passwords')
.action((options: any) => {
passwordManager.listPasswords(options.show);
});
program.parse();
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create CLI tools with Commander.js and TypeScript ๐ช
- โ Define type-safe commands and options ๐ก๏ธ
- โ Build interactive CLIs with user prompts ๐ฏ
- โ Handle async operations properly ๐
- โ Create professional developer tools with TypeScript! ๐
Remember: A great CLI is intuitive, helpful, and type-safe! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered CLI development with Commander.js and TypeScript!
Hereโs what to do next:
- ๐ป Build your own CLI tool using the patterns above
- ๐๏ธ Explore advanced features like custom help and hooks
- ๐ Move on to our next tutorial: Testing Your CLI Applications
- ๐ Share your CLI creations with the developer community!
Remember: Every great tool started as a simple command. Keep building, keep learning, and most importantly, have fun! ๐
Happy CLI coding! ๐๐โจ