Yargs: statically typed builder commands
Yargs is a popular library for building command line interfaces in Node.js. And the name is just fabulous. Yargs provides a way to define commands, options, and arguments in a structured way. However, Yargs has been around for a long time and it the documentation makes little mention of TypeScript support.
Whilst there is some documentation, if you're building more involved command line interfaces with Yargs in TypeScript, you may find that you need to do a bit of extra work to get strong typing working well with commands that have builders. In this post, I'll demonstrate how to use Yargs to create statically typed commands with builders in TypeScript.
Before we start, I should say that I'm working with Yargs version 18.0.0 in this post. The type definitions come from Definitely Typed and the version is 17.0.35. However, there is no significant difference in the types between Yargs 17 and 18 and so the difference is not an issue.
As an aside, it's possibly worth mentioning that these days it's possible to go without third party libraries entirely to parse command line arguments thanks to features like parseArgs which have been part of Node.js since version 18. However, Yargs remains a popular choice and is still widely used. I have no plans to replace Yargs in my existing projects just yet.
Getting the builders in
Let's start with a simple example. Imagine we want to create a command line tool that has a number of commands. Each command has its own options, and we want to use builders to define those options. Here's how we might set that up with Yargs, first we have a main entry point:
As we can see, this imports a single command called mySimpleCommand. Let's look at how that command is defined:
You can see we have a command called simple-command that has a single option called myOption in the builder. However, if we look at the handler function, we can see that myOption is of type unknown. This is because Yargs does not know the shape of the arguments that will be passed to the handler.
This is the problem we need to solve. Inside the handler, we want to have statically typed access to the options defined in the builder.
Statically typing command builders
To achieve strong typing, we can define an interface that describes the shape of the arguments for our command. We can then use this interface to type the CommandModule. Here's how we can modify the mySimpleCommand to achieve this:
There's three things to note here:
- We've defined an Args interface that describes the shape of the arguments for the command.
- We've updated the CommandModule type to use Args as the second type, which defines the return type of the builder.
- In the builder, we've specified the type of myOption as string. This is crucial for strong typing to work correctly. Without this we will have compilation errors from TypeScript.
Now we have statically typed access to myOption inside the handler. Yay!
Sharing options among commands and builders
It's not unusual to have options that are shared among multiple commands. Imagine a common option that all commands need to use. How can we share that option definition among multiple commands while maintaining strong typing? We can achieve this by defining a shared interface for the common options and a function that adds those options to a builder. Here's how we can do that:
Then, in our command files, we can import the interface and the function and use them like this:
By following this pattern, we can create statically typed commands with builders in Yargs while also sharing common options among multiple commands.
It's a beautiful pattern that sparks joy in my soul. Happy coding!
Discussion in the ATmosphere