deno-cliffy
deno-cliffy copied to clipboard
Feature: Support extracting options/args types from Command.
Context
For complicated CLIs, there can be lots of .action(…)
s, and having your command implementation bodies indented inside of the Command
builder is not always ideal. Even for small projects, I prefer this pattern which decouples my implementation from the arg parsing:
const COMMAND = new Command()
// etc, global setup.
async function subCommand(options: MyOptions, args: [string, etc]) {
// …
}
COMMAND.command("subcommand")
// options
.action(subCommand)
But to do this as of now (unless I'm missing something? 😅) I have to define my own Options
type which conforms to the options that Command.action()
will pass me.
Feature request
It would be nice if Command supported an "infer" like Zod.
In Zod, you do something like:
type A = z.infer<typeof A>;
Maybe in Cliffy's Command, we could do something like:
const SUBCOMMAND = COMMAND.command("subcommand")
// options
.action(subCommand)
type Options = Command.inferOptions<typeof SUBCOMMAND>
type Args = Command.inferArguments<typeof SUBCOMMAND>
The implementation of z.infer
is not something that I've looked into, so I'm not sure how easy it would be to apply to Command. But as a user it would be nice to have. 😊 Thanks!
Hi @NfNitLoop, sry for late replay, i'm on a long trip currently.
I think this is a good idea. We have already Type.infer
which works similarly. One way of implementing inferOptions
and inferArguments
could be to extract the types from the action handler.
Is there currently any way to extract the action handler types? I am currently running into a similar issue when using a globalOption
. I can define types manually but this becomes tedious if you have many options.
For example, in main entry:
await new Command()
.globalOption('-p, --project-path <project-path:file>', 'Path to project folder', {
default: config.projectPath,
})
.command('init', Init)
.parse(Deno.args)
And init.ts
in a different file:
export const Init = new Command()
.description('init')
.action(async (options) => {
// This works but type error
// `Property 'projectPath' does not exist on type 'void`
const { projectPath } = options
})
@xe54ck You can use the infer type.
For example, you can do something like this:
entry.ts
export type GlobalOptions = typeof args extends
Command<void, void, void, [], infer Options extends Record<string, unknown>>
? Options
: never;
const args = new Command()
.globalOption(
"-p, --project-path <project-path:file>",
"Path to project folder",
{
default: config.projectPath,
},
);
await args
.command("init", Init)
.parse();
init.ts
import type { GlobalOptions } from "./entry.ts";
export const Init = new Command<GlobalOptions>()
.description("init")
.action(async (options) => {
// This works but type error
// `Property 'projectPath' does not exist on type 'void`
const { projectPath } = options;
});
Thank you, @DrakeTDL! That worked great, been looking for something like this for a while! 😊