Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Command typings #1758

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c8629b8
Add generic Command type
PaperStrike Jun 21, 2022
7fcf574
Fix string argument type helpers
PaperStrike Jun 21, 2022
9837001
Support Command.arguments type parsing
PaperStrike Jun 21, 2022
d9c62bb
Add StringUntypedOption type helper
PaperStrike Jun 21, 2022
92e5e9f
Simplify string argument type helpers
PaperStrike Jun 21, 2022
6f26198
Remove type helper default type requirements
PaperStrike Jun 21, 2022
606d438
Properly detect possibly undefined default type
PaperStrike Jun 21, 2022
b79504e
Remove "default type" arg of StringArguments
PaperStrike Jun 21, 2022
bcbe369
Clean unused vars
PaperStrike Jun 21, 2022
f49deec
Parse unspaced ",", ending spaces, in option flags
PaperStrike Jun 22, 2022
3e1a608
Add Argument and Option generic type
PaperStrike Jun 22, 2022
6bfe45d
Type required, optional, variadic, mandatory
PaperStrike Jun 22, 2022
806422f
Merge options to a single object
PaperStrike Jun 22, 2022
eac7d1e
Type .opts return default to Options
PaperStrike Jun 22, 2022
a47e0ef
Allow .action callback type argument override
PaperStrike Jun 22, 2022
364e04e
Type .command arguments
PaperStrike Jun 22, 2022
5502907
Type .command call that has no argument string
PaperStrike Jun 22, 2022
9936b2d
Fix .command call related to .createCommand
PaperStrike Jun 22, 2022
1d137cc
Allow no brackets around argument name and default to required option
PaperStrike Jun 22, 2022
cb2cda2
Fix types of any Option / any Argument
PaperStrike Jun 22, 2022
d4b3acd
Make type test pass
PaperStrike Jun 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 119 additions & 50 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,67 @@
/* eslint-disable @typescript-eslint/method-signature-style */
/* eslint-disable @typescript-eslint/no-explicit-any */

type CamelCase<S extends string> = S extends `${infer W}-${infer Rest}`
? CamelCase<`${W}${Capitalize<Rest>}`>
: S;

type StringImpliedType<S extends string, /* fallback to boolean */ F extends boolean = false> =
S extends `${string}<${string}...>`
? [string, ...string[]]
: S extends `${string}[${string}...]`
? F extends true
? string[] | boolean
: string[]
: S extends `${string}<${string}>`
? string
: S extends `${string}[${string}]`
? F extends true
? string | boolean
: string | undefined
: F extends true
? boolean
: string;

type StringTypedArgument<S extends string, T, /* default type */ D> =
undefined extends D
? S extends `[${string}]`
? T | undefined
: T
: T;

type StringUntypedArgument<S extends string, /* default type */ D> =
undefined extends D
? StringImpliedType<S>
: NonNullable<StringImpliedType<S>>;

type StringArguments<S extends string> =
S extends `${infer A} ${infer Rest}`
? [StringUntypedArgument<A, undefined>, ...StringArguments<Rest>]
: [StringUntypedArgument<S, undefined>];

type StringCommand<S extends string> =
S extends `${string} ${infer Rest}`
? StringArguments<Rest>
: [];

type StringTypedOption<S extends string, T, /* default type */ D> =
S extends `${infer Flags} <${string}>` | `${infer Flags} [${string}]` | `${infer Flags} ` // Trim the ending ` <xxx>` or ` [xxx]` or ` `
? StringTypedOption<Flags, T, D>
: S extends `-${string},${infer Rest}` // Trim the leading `-xxx,`
? StringTypedOption<Rest, T, D>
: S extends `-${infer Rest}` | ` ${infer Rest}` // Trim the leading `-` or ' '.
? StringTypedOption<Rest, T, D>
: S extends `no-${infer Rest}` // Check the leading `no-`
? { [K in CamelCase<Rest>]: T }
: undefined extends D
? { [K in CamelCase<S>]?: T }
: { [K in CamelCase<S>]: T };

type StringUntypedOption<S extends string, /* default type */ D> =
StringTypedOption<S, StringImpliedType<S, true>, D>;

type MergeOptions<A, B> = (A & B) extends infer O ? {[K in keyof O]: O[K]} : never;

export class CommanderError extends Error {
code: string;
exitCode: number;
Expand Down Expand Up @@ -38,7 +99,7 @@ export interface ErrorOptions { // optional parameter for error()
exitCode?: number;
}

export class Argument {
export class Argument<Arg extends string, T = StringImpliedType<Arg>, D = undefined> {
description: string;
required: boolean;
variadic: boolean;
Expand All @@ -48,7 +109,7 @@ export class Argument {
* The default is that the argument is required, and you can explicitly
* indicate this with <> around the name. Put [] around the name for an optional argument.
*/
constructor(arg: string, description?: string);
constructor(arg: Arg, description?: string);

/**
* Return argument name.
Expand All @@ -58,12 +119,12 @@ export class Argument {
/**
* Set the default value, and optionally supply the description to be displayed in the help.
*/
default(value: unknown, description?: string): this;
default<D2>(value: D2, description?: string): Argument<Arg, T, D2>;

/**
* Set the custom handler for processing CLI command arguments into argument values.
*/
argParser<T>(fn: (value: string, previous: T) => T): this;
argParser<T2>(fn: (value: string, previous: T2) => T2): Argument<Arg, T2, D>;

/**
* Only allow argument value to be one of choices.
Expand All @@ -81,30 +142,30 @@ export class Argument {
argOptional(): this;
}

export class Option {
flags: string;
export class Option<Flags extends string, T = StringImpliedType<Flags, true>, D = undefined, M = false> {
flags: Flags;
description: string;

required: boolean; // A value must be supplied when the option is specified.
optional: boolean; // A value is optional when the option is specified.
variadic: boolean;
mandatory: boolean; // The option must have a value after parsing, which usually means it must be specified on command line.
required: Flags extends `${string}<${string}>${string}` ? true : false; // A value must be supplied when the option is specified.
optional: Flags extends `${string}<${string}>${string}` ? false : true; // A value is optional when the option is specified.
variadic: Flags extends `${string}[${string}...]${string}` | `${string}<${string}...>${string}` ? true : false;
mandatory: M; // The option must have a value after parsing, which usually means it must be specified on command line.
optionFlags: string;
short?: string;
long?: string;
short: string | undefined;
long: string | undefined;
negate: boolean;
defaultValue?: any;
defaultValueDescription?: string;
parseArg?: <T>(value: string, previous: T) => T;
defaultValue: D;
defaultValueDescription: string | undefined;
parseArg: ((value: string, previous: T) => T) | undefined;
hidden: boolean;
argChoices?: string[];
argChoices: string[] | undefined;

constructor(flags: string, description?: string);
constructor(flags: Flags, description?: string);

/**
* Set the default value, and optionally supply the description to be displayed in the help.
*/
default(value: unknown, description?: string): this;
default<D2>(value: D2, description?: string): Option<Flags, T, D2, M>;

/**
* Preset to use when option used without option-argument, especially optional but also boolean and negated.
Expand Down Expand Up @@ -156,12 +217,12 @@ export class Option {
/**
* Set the custom handler for processing CLI option arguments into option values.
*/
argParser<T>(fn: (value: string, previous: T) => T): this;
argParser<T2>(fn: (value: string, previous: T2) => T2): Option<Flags, T2, D, M>;

/**
* Whether the option is mandatory and must have a value after parsing.
*/
makeOptionMandatory(mandatory?: boolean): this;
makeOptionMandatory<M2 extends boolean = true>(mandatory?: M2): Option<Flags, T, D, M2>;

/**
* Hide option in help.
Expand Down Expand Up @@ -205,13 +266,13 @@ export class Help {
/** Get the command summary to show in the list of subcommands. */
subcommandDescription(cmd: Command): string;
/** Get the option term to show in the list of options. */
optionTerm(option: Option): string;
optionTerm(option: Option<any, any, any, any>): string;
/** Get the option description to show in the list of options. */
optionDescription(option: Option): string;
optionDescription(option: Option<any, any, any, any>): string;
/** Get the argument term to show in the list of arguments. */
argumentTerm(argument: Argument): string;
argumentTerm(argument: Argument<any, any, any>): string;
/** Get the argument description to show in the list of arguments. */
argumentDescription(argument: Argument): string;
argumentDescription(argument: Argument<any, any, any>): string;

/** Get the command usage to be displayed at the top of the built-in help. */
commandUsage(cmd: Command): string;
Expand All @@ -221,9 +282,9 @@ export class Help {
/** Get an array of the visible subcommands. Includes a placeholder for the implicit help command, if there is one. */
visibleCommands(cmd: Command): Command[];
/** Get an array of the visible options. Includes a placeholder for the implicit help option, if there is one. */
visibleOptions(cmd: Command): Option[];
visibleOptions(cmd: Command): Array<Option<any, any, any, any>>;
/** Get an array of the arguments which have descriptions. */
visibleArguments(cmd: Command): Argument[];
visibleArguments(cmd: Command): Array<Argument<any, any, any>>;

/** Get the longest command term length. */
longestSubcommandTermLength(cmd: Command, helper: Help): number;
Expand Down Expand Up @@ -272,7 +333,7 @@ export interface OptionValues {
[key: string]: any;
}

export class Command {
export class Command<Args extends unknown[] = [], Options extends { [K: string]: unknown } = {}> {
args: string[];
processedArgs: any[];
commands: Command[];
Expand Down Expand Up @@ -310,7 +371,7 @@ export class Command {
* @param opts - configuration options
* @returns new command
*/
command(nameAndArgs: string, opts?: CommandOptions): ReturnType<this['createCommand']>;
command<T extends string>(nameAndArgs: T, opts?: CommandOptions): (<T>() => T extends this['createCommand'] ? 1 : 2) extends (<T>() => T extends Command['createCommand'] ? 1 : 2) ? Command<StringCommand<T>> : ReturnType<this['createCommand']>;
/**
* Define a command, implemented in a separate executable file.
*
Expand All @@ -329,7 +390,7 @@ export class Command {
* @param opts - configuration options
* @returns `this` command for chaining
*/
command(nameAndArgs: string, description: string, opts?: ExecutableCommandOptions): this;
command<T extends string>(nameAndArgs: T, description: string, opts?: ExecutableCommandOptions): Command<StringCommand<T>>;

/**
* Factory routine to create a new unattached command.
Expand All @@ -354,7 +415,7 @@ export class Command {
* See .argument() for creating an attached argument, which uses this routine to
* create the argument. You can override createArgument to return a custom argument.
*/
createArgument(name: string, description?: string): Argument;
createArgument<Arg extends string>(name: Arg, description?: string): Argument<Arg>;

/**
* Define argument syntax for command.
Expand All @@ -370,15 +431,17 @@ export class Command {
*
* @returns `this` command for chaining
*/
argument<T>(flags: string, description: string, fn: (value: string, previous: T) => T, defaultValue?: T): this;
argument(name: string, description?: string, defaultValue?: unknown): this;
argument<Flags extends string, T>(flags: Flags, description: string, fn: (value: string, previous: T) => T): Command<[...Args, StringTypedArgument<Flags, T, undefined>], Options>;
argument<Flags extends string, T, D extends T | undefined>(flags: Flags, description: string, fn: (value: string, previous: T) => T, defaultValue: D): Command<[...Args, StringTypedArgument<Flags, T, D>], Options>;
argument<Name extends string>(name: Name, description?: string): Command<[...Args, StringUntypedArgument<Name, undefined>], Options>;
argument<Name extends string, D extends StringImpliedType<Name> | undefined>(name: Name, description: string, defaultValue: D): Command<[...Args, StringUntypedArgument<Name, D>], Options>;

/**
* Define argument syntax for command, adding a prepared argument.
*
* @returns `this` command for chaining
*/
addArgument(arg: Argument): this;
addArgument<Arg extends string, T, D>(arg: Argument<Arg, T, D>): Command<[...Args, StringTypedArgument<Arg, T, D>], Options>;

/**
* Define argument syntax for command, adding multiple at once (without descriptions).
Expand All @@ -392,7 +455,7 @@ export class Command {
*
* @returns `this` command for chaining
*/
arguments(names: string): this;
arguments<Names extends string>(names: Names): Command<[...Args, ...StringArguments<Names>], Options>;

/**
* Override default decision whether to add implicit help command.
Expand Down Expand Up @@ -489,7 +552,8 @@ export class Command {
*
* @returns `this` command for chaining
*/
action(fn: (...args: any[]) => void | Promise<void>): this;
action(fn: (this: this, ...args: [...Args, Options, this]) => void | Promise<void>): this;
action<A extends unknown[], O extends OptionValues>(fn: (this: this, ...args: [...A, ...Args, O & Options, this]) => void | Promise<void>): this;

/**
* Define option with `flags`, `description` and optional
Expand Down Expand Up @@ -535,8 +599,10 @@ export class Command {
*
* @returns `this` command for chaining
*/
option(flags: string, description?: string, defaultValue?: string | boolean | string[]): this;
option<T>(flags: string, description: string, fn: (value: string, previous: T) => T, defaultValue?: T): this;
option<Flags extends string>(flags: Flags, description?: string): Command<Args, MergeOptions<Options, StringUntypedOption<Flags, undefined>>>;
option<Flags extends string, D extends StringImpliedType<Flags, true> | undefined>(flags: Flags, description: string, defaultValue: D): Command<Args, MergeOptions<Options, StringUntypedOption<Flags, D>>>;
option<Flags extends string, T>(flags: Flags, description: string, fn: (value: string, previous: T) => T): Command<Args, MergeOptions<Options, StringTypedOption<Flags, T, undefined>>>;
option<Flags extends string, T, D extends T | undefined>(flags: Flags, description: string, fn: (value: string, previous: T) => T, defaultValue: D): Command<Args, MergeOptions<Options, StringTypedOption<Flags, T, D>>>;
/** @deprecated since v7, instead use choices or a custom function */
option(flags: string, description: string, regexp: RegExp, defaultValue?: string | boolean | string[]): this;

Expand All @@ -546,8 +612,10 @@ export class Command {
*
* The `flags` string contains the short and/or long flags, separated by comma, a pipe or space.
*/
requiredOption(flags: string, description?: string, defaultValue?: string | boolean | string[]): this;
requiredOption<T>(flags: string, description: string, fn: (value: string, previous: T) => T, defaultValue?: T): this;
requiredOption<Flags extends string>(flags: Flags, description?: string): Command<Args, MergeOptions<Options, Required<StringUntypedOption<Flags, undefined>>>>;
requiredOption<Flags extends string, D extends StringImpliedType<Flags, true> | undefined>(flags: Flags, description: string, defaultValue: D): Command<Args, MergeOptions<Options, Required<StringUntypedOption<Flags, D>>>>;
requiredOption<Flags extends string, T>(flags: Flags, description: string, fn: (value: string, previous: T) => T): Command<Args, MergeOptions<Options, Required<StringTypedOption<Flags, T, undefined>>>>;
requiredOption<Flags extends string, T, D extends T | undefined>(flags: Flags, description: string, fn: (value: string, previous: T) => T, defaultValue: D): Command<Args, MergeOptions<Options, Required<StringTypedOption<Flags, T, D>>>>;
/** @deprecated since v7, instead use choices or a custom function */
requiredOption(flags: string, description: string, regexp: RegExp, defaultValue?: string | boolean | string[]): this;

Expand All @@ -558,39 +626,39 @@ export class Command {
* create the option. You can override createOption to return a custom option.
*/

createOption(flags: string, description?: string): Option;
createOption<Flags extends string>(flags: Flags, description?: string): Option<Flags>;

/**
* Add a prepared Option.
*
* See .option() and .requiredOption() for creating and attaching an option in a single call.
*/
addOption(option: Option): this;
addOption<Flags extends string, T, D, M>(option: Option<Flags, T, D, M>): Command<Args, MergeOptions<Options, M extends true ? Required<StringTypedOption<Flags, T, D>> : StringTypedOption<Flags, T, D>>>;

/**
* Whether to store option values as properties on command object,
* or store separately (specify false). In both cases the option values can be accessed using .opts().
*
* @returns `this` command for chaining
*/
storeOptionsAsProperties<T extends OptionValues>(): this & T;
storeOptionsAsProperties<T extends OptionValues>(storeAsProperties: true): this & T;
storeOptionsAsProperties<T extends OptionValues>(): this & T & Options;
storeOptionsAsProperties<T extends OptionValues>(storeAsProperties: true): this & T & Options;
storeOptionsAsProperties(storeAsProperties?: boolean): this;

/**
* Retrieve option value.
*/
getOptionValue(key: string): any;
getOptionValue<K extends string>(key: K): Options[K];

/**
* Store option value.
*/
setOptionValue(key: string, value: unknown): this;
setOptionValue<K extends string, V>(key: K, value: V): Command<Args, { [OK in keyof Options]: OK extends K ? V : Options[OK] }>;

/**
* Store option value and where the value came from.
*/
setOptionValueWithSource(key: string, value: unknown, source: OptionValueSource): this;
setOptionValueWithSource<K extends string, V>(key: K, value: V, source: OptionValueSource): Command<Args, { [OK in keyof Options]: OK extends K ? V : Options[OK] }>;

/**
* Retrieve option value source.
Expand Down Expand Up @@ -697,12 +765,13 @@ export class Command {
/**
* Return an object containing local option values as key-value pairs
*/
opts<T extends OptionValues>(): T;
opts<T extends OptionValues>(): T & Options;
opts(): Options;

/**
* Return an object containing merged local and global option values as key-value pairs.
*/
optsWithGlobals<T extends OptionValues>(): T;
optsWithGlobals<T extends OptionValues>(): T & Options;

/**
* Set the description.
Expand Down Expand Up @@ -873,7 +942,7 @@ export interface ParseOptionsResult {
}

export function createCommand(name?: string): Command;
export function createOption(flags: string, description?: string): Option;
export function createArgument(name: string, description?: string): Argument;
export function createOption<Flags extends string>(flags: Flags, description?: string): Option<Flags>;
export function createArgument<Arg extends string>(name: Arg, description?: string): Argument<Arg>;

export const program: Command;
Loading