Creating a CLI with TypeScript

With a tiny bit of configuration, the same statically-typed goodness we’re used to in web applications is just as accessible–and just as helpful–for building command line interfaces. TypeScript simplifies JavaScript at scale. Who said it had to be in the browser?

Configuration

First things first: let’s set up a new project

$ mkdir ts-cli && cd ts-cli
$ npm init # etc
$ npm install --save-dev @types/node

We’ll also want to add a simple tsconfig.json for the TypeScript compiler.

{
  "compilerOptions": {
    "target": "es2015",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true
  }
}

Finally, we’ll need a runtime. Some day deno will be neatly packaged up for public consumption. Some day. In the meantime, ts-node will do the trick:

$ npm install -g ts-node

Configuration, check. We’re ready to set about building the CLI.

The CLI

Here’s scaffolding for a command-line interface with two commands, echo and help:

#!/usr/bin/env ts-node

import * as path from 'path';

type Command = (...args: string[]) => Promise<void>;

const CLIFILE = path.relative(process.cwd(), process.argv[1]);

const commandUsage = (subcmd: string, details: string) => {
  console.log(`Usage: ${CLIFILE} ${subcmd} ${details}`);
  process.exit(2);
};

const commands: { [s: string]: Command } = {
  async help() {
    usage();
  },
  async echo(...args: string[]) {
    if (args.length < 1) {
      commandUsage('echo', '<string>');
    }
    console.log(args.join(' '));
  },
};

const usage = () => {
  const commandKeys = Object.keys(commands);
  console.log(`Demo TypeScript CLI

Usage: ./${CLIFILE} ${commandKeys.join('|')}
`);
};

const key = process.argv[2];
const handler = commands[key];
if (typeof handler !== 'function') {
  usage();
  process.exit(2);
}

commands[key](...process.argv.slice(3));

Like any good interface, most of the code here is error-handling and documentation. There’s a usage function to describe the interface’s commands, a small wrapper for commandUsage, and a tiny bit of bootstrap logic to kick it all off. Still, it’s enough to prove the toolchain and provide a jumping-off point for more sophisticated TypeScript applications.

Let’s set the executable bit and see how things go.

$ chmod +x cli.ts

# See usage information
$ ./cli.ts
Demo TypeScript CLI

Usage: ./cli.ts help|echo

# And actually echo something
$ ./cli.ts echo 'Hello, world!'
Hello, world!

Perfect. With less than five minutes’ work we’ve got ourselves a CLI. From here, we can add more commands, optional flags, or whatever other inputs our application needs–now with static type-checking all the way down!

Hey, I'm RJ! For more learnings about software and management, find me @rjzaworski or sign up for my semi-regular newsletter.

Let’s keep in touch

Send me timely updates on software, product, and process.