c4spar / deno-cliffy

Command line framework for deno 🦕 Including Commandline-Interfaces, Prompts, CLI-Table, Arguments Parser and more...

Home Page:https://cliffy.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Feature: Support extracting options/args types from Command.

NfNitLoop opened this issue · comments

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! 😊