How to Build a Simple CLI Using TypeScript and Factory Design Pattern

Thanoshan MV
Level Up Coding
Published in
7 min readMay 18, 2021

--

Before jumping into the development of complex CLI applications with advanced procedures and make use of other packages and libraries/tools, first, we need to understand the core concepts of making a CLI application.

After reading this article, you’ll understand the concepts of making a CLI, factory design pattern, and will be able to create CLI applications using any language.

In this article, we’ll see how we can create a simple TypeScript CLI that can run using Node.js.😃

Application Overview

Before going to the implementation, we’ll take a look at how our application will be constructed.

Figure 1: Class Diagram of this CLI

In the main application, we’ll get each command using the command name from the command factory. This is known as the factory design pattern.

Factory Design Pattern

In this pattern, we’ll have a factory. That factory will give us different types of objects which all are under a common type. We’ll get these different types of objects from the factory by providing some unique identifiers. Usually, these unique identifiers will be strings.

So, as a client, we don’t know how those objects are constructed inside the factory. We’ll get these usable objects from the factory as promised and they can be utilized in the application.

Since the factory can construct and give us all different types of objects based on our request, we point to those objects via the common type.

This pattern provides one of the best ways to create objects as:

  1. The client doesn’t know the object creation logic.
  2. We don’t have to create objects in the main application.

In our CLI, we’ll call each command by providing the command’s name to the command factory.

Let’s dive into creating CLI.

1. Initial Setup

Go to the application directory and create a new NPM package using npm init --y.

A typical CLI application has one or more executable files and they will be executed when we type application-specific commands.

In our application, we’ll have one executable file and that will be executed when we type the command we .

How entering we executes the executable file? We map the command we with the executable file. So whenever we enter the command we that will execute the mapped executable file. That’s the idea here.

Command name mapping with the executable file is achieved by specifying bin field in our package.json.

Next, let’s create the entry point of the application (which actually gets the user arguments). Create a directory bin and create a file named we . Add the following properties to package.json file.

"bin": {
"we": "./bin/we"
},
"main": "./bin/we"

As previously mentioned, “we”: “./bin/we” maps the command we with ./bin/we . On global install, npm will symlink that ./bin/we into prefix/bin, and ./node_modules/.bin/ for local installs.

The “main” field tells “./bin/we” is the primary entry point to our program.

we is the command, we use to execute the executable file “./bin/we” .

To make ./bin/we as an executable file, add the following line at the beginning of the file:

#!/usr/bin/env node

The above line tells the system that this file is a node executable file. Now our executable file is ready.

We can’t directly run TypeScript files using Node.js. We need to compile down TypeScript files into JavaScript files. This can be easily configured using TypeScript configuration file.

Let’s create a TypeScript configuration file using tsc --init . Here, we’ll need to specify the root directory of input files that will contain TypeScript files: “rootDir”: “./src” and output directory which will contain compiled JavaScript files: “outDir”: “./lib” . You can add/remove other properties in tsconfig.json based on your needs.

As per the TypeScript configuration, the TypeScript files inside src directory will be compiled into lib directory as JavaScript files.

Our entry file ./bin/we should execute compiled JavaScript files inside lib directory. To specify that add the following lines in the we file:

#!/usr/bin/env node
// require the compiled js files from ts
require("../lib/we.js");

2. Basic CLI

Let’s install dependencies:

  1. Type definitions for node: npm i — save-dev @types/node
  2. TypeScript in the project: npm install typescript — save-dev

Create a global symlink using npm link .

Create a new directory src inside the root directory. This src directory will contain all of our TypeScript files.

Create we.ts inside src .

First, we’ll get all the CLI arguments when the Node.js process was launched by adding the following code to we.ts :

const args = process.argv;
// log all CLI arguments
console.log(args);

process.argv returns an array containing all the CLI arguments.

To check this, we should compile the TypeScript into JavaScript so that our executable file can execute the JavaScript code inside lib directory.

So, let’s define a script for compiling the project in the package.json :

"build":  "tsc"

Since we have tsconfig.json configured, we don’t have to provide commands like: tsc src/*.ts . For more detailed information, check out this Stack Overflow discussion.

Let’s compile the project using npm run build . It'll create compiled JavaScript files inside the lib directory as specified in the tsconfig file.

Enter we to check CLI arguments:

Figure 2: CLI arguments

It returns two arguments. First is the absolute pathname of the executable that started the Node.js process. The second is the path to the JavaScript file being executed.

We don’t need these arguments. We only need one argument which is entered by the user.

console.log(args.slice(2));
Figure 3: User entered keywords

Now, we’re able to get user input from CLI.

As specified in the class diagram, our application will have three main commands: we hi , we status , and we help .

If the user just types we , we’ll display him with a nice help guide.

// the help guide
const helpGuide = function () {
const helpText = `
we is a friendly CLI!
usage:
we <command>
commands can be:
hi: used to welcome to the user
help: used to print the usage guide
`;
console.log(helpText);
};
// if user doesn't enter any words (just types 'we')
if (userArgs.length == 0) {
helpGuide();
}
Figure 4: The output of the help guide

Anything other than that, we’ll process it.

else {
console.log('call the command factory and get appropriate command');
}
Figure 5: Processing else part

Now, our application is ready to call the command factory 😃

3. Build Command Factory and Commands

Let’s create commands directory inside the src folder. This directory will contain all the commands associated with our application.

Create a common command interface Command.ts :

export interface Command { run(): void; }

Let’s create specific commands.

HelpCommand.ts :

export class HelpCommand implements Command {
run(): void {
const helpText = `
we is a friendly CLI!
usage:
we <command>
commands can be:
hi: used to welcome to the user
help: used to print the usage guide
`;
console.log(helpText);
}
}

HiCommand.ts :

export class HiCommand implements Command {
run(): void {
console.log(`Welcome to we CLI!`);
}
}

StatusCommand.ts :

export class StatusCommand implements Command {
run(): void {
console.log(`Checkout the status`);
}
}

ErrorCommand.ts :

export class ErrorCommand implements Command {
run(): void {
console.log(`Sorry! No keywords found. Please type 'Help' to see what I can help you with :)`);
}
}

Let’s create the command factory inside src directory:

CommandFactory.ts :

export class CommandFactory {
getCommand(commandName: string): Command {
switch(commandName) {
case "hi":
return new HiCommand();
case "status":
return new StatusCommand();
case "help":
return new HelpCommand();
default:
return new ErrorCommand();
}
}
}

Our commands and command factory is ready.

Let’s call our command factory from we.ts :

else {  
// call the command factory and get appropriate command
const factoryObject = new CommandFactory();
const commandObject = factoryObject.getCommand(userArgs[0]);
commandObject.run();
}

Now, everything is set. Let’s build the project and test the commands!

Figure 6: Build and test the commands

Great! We have a complete working CLI implemented using TypeScript! 😄 Now, we can further extend this CLI application based on our wishes.

Check out the source code for this CLI application on GitHub.

Conclusion

Thank you for reading! I hope this article helped you.

I thank Shalitha Suranga for helping me out! 🙌

Happy Coding ❤️

--

--