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

Rework Argument generic types to retain chaining information #31

Merged
merged 2 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 28 additions & 20 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,25 @@ type InferArgumentOptionalType<Value extends string, DefaultT, CoerceT> =
Value extends `${string}...`
? InferArgumentType<Value, [DefaultT] extends [undefined] ? never : DefaultT, CoerceT>
: InferArgumentType<Value, DefaultT, CoerceT>

type InferArgument<S extends string, DefaultT = undefined, CoerceT = undefined> =
S extends `<${infer Value}>`

// ArgRequired comes from .argRequired()/.argOptional(), and ArgRequiredFromUsage is implied by usage <required>/[optional]
type ResolveRequired<ArgRequired extends boolean|undefined, ArgRequiredFromUsage extends boolean> =
ArgRequired extends undefined
? ArgRequiredFromUsage
: ArgRequired;

type InferArgumentTypeResolvedRequired<Value extends string, DefaultT, CoerceT, ArgRequired extends boolean> =
ArgRequired extends true
? InferArgumentType<Value, never, CoerceT>
: InferArgumentOptionalType<Value, DefaultT, CoerceT>;

// Resolve whether argument required, and strip []/<> from around value.
type InferArgument<S extends string, DefaultT = undefined, CoerceT = undefined, ArgRequired extends boolean|undefined = undefined> =
S extends `<${infer Value}>`
? InferArgumentTypeResolvedRequired<Value, DefaultT, CoerceT, ResolveRequired<ArgRequired, true>>
: S extends `[${infer Value}]`
? InferArgumentOptionalType<Value, DefaultT, CoerceT>
: InferArgumentType<S, never, CoerceT>; // the implementation fallback is treat as <required>
? InferArgumentTypeResolvedRequired<Value, DefaultT, CoerceT, ResolveRequired<ArgRequired, false>>
: InferArgumentTypeResolvedRequired<S, DefaultT, CoerceT, ResolveRequired<ArgRequired, true>>; // the implementation fallback is treat as <required>

type InferArguments<S extends string> =
S extends `${infer First} ${infer Rest}`
Expand All @@ -57,11 +69,6 @@ type InferCommmandArguments<S extends string> =
? InferArguments<TrimLeft<Args>>
: [];

type NullableCopy<T, U> =
U extends undefined
? T | undefined
: T;

type FlagsToFlag<Flags extends string> =
Flags extends `${string},${infer LongFlag}`
? TrimLeft<LongFlag>
Expand Down Expand Up @@ -140,10 +147,10 @@ type InferOptionsNegateCombo<Options, Flag extends string, Name extends string,
// Fill in appropriate PresetT value if undefined.
type InferOptionTypes<Options, Flag extends string, Value extends string, ValueT, PresetT, DefaultT, CoerceT, Mandatory extends boolean> =
InferOptionsNegateCombo<Options, Flag, ConvertFlagToName<Flag>,
CoerceValueType<CoerceT, InferVariadic<Value, ValueT>>,
NegatePresetType<Flag, CoercePresetType<CoerceT, PresetT>>,
NegateDefaultType<Flag, DefaultT>,
IsAlwaysDefined<DefaultT, Mandatory>>;
CoerceValueType<CoerceT, InferVariadic<Value, ValueT>>,
NegatePresetType<Flag, CoercePresetType<CoerceT, PresetT>>,
NegateDefaultType<Flag, DefaultT>,
IsAlwaysDefined<DefaultT, Mandatory>>;

type InferOptionsFlag<Options, Flags extends string, Value extends string, ValueT, PresetT, DefaultT, CoerceT, Mandatory extends boolean> =
InferOptionTypes<Options, FlagsToFlag<Trim<Flags>>, Trim<Value>, ValueT, PresetT, DefaultT, CoerceT, Mandatory>;
Expand Down Expand Up @@ -197,7 +204,7 @@ export class CommanderError extends Error {
exitCode?: number;
}

export class Argument<Usage extends string = '', ArgType = InferArgument<Usage>> {
export class Argument<Usage extends string = '', DefaultT = undefined, CoerceT = undefined, ArgRequired extends boolean|undefined = undefined> {
description: string;
required: boolean;
variadic: boolean;
Expand All @@ -217,12 +224,12 @@ export class CommanderError extends Error {
/**
* Set the default value, and optionally supply the description to be displayed in the help.
*/
default<T>(value: T, description?: string): Argument<string, NonNullable<ArgType> | T>;
default<T>(value: T, description?: string): Argument<Usage, T, CoerceT, ArgRequired>;

/**
* Set the custom handler for processing CLI command arguments into argument values.
*/
argParser<T>(fn: (value: string, previous: T) => T): Argument<string, NullableCopy<T, ArgType>>;
argParser<T>(fn: (value: string, previous: T) => T): Argument<Usage, DefaultT, T, ArgRequired>;

/**
* Only allow argument value to be one of choices.
Expand All @@ -232,12 +239,12 @@ export class CommanderError extends Error {
/**
* Make argument required.
*/
argRequired(): Argument<string, NonNullable<ArgType>>;
argRequired(): Argument<Usage, DefaultT, CoerceT, true>;

/**
* Make argument optional.
*/
argOptional(): Argument<string, ArgType | undefined>;
argOptional(): Argument<Usage, DefaultT, CoerceT, false>;
}

export class Option<Usage extends string = '', PresetT = undefined, DefaultT = undefined, CoerceT = undefined, Mandatory extends boolean = false> {
Expand Down Expand Up @@ -551,7 +558,8 @@ export class CommanderError extends Error {
*
* @returns `this` command for chaining
*/
addArgument<S extends string, ArgType>(arg: Argument<S, ArgType>): Command<[...Args, ArgType]>;
addArgument<Usage extends string, DefaultT, CoerceT, ArgRequired extends boolean|undefined>(
arg: Argument<Usage, DefaultT, CoerceT, ArgRequired>): Command<[...Args, InferArgument<Usage, DefaultT, CoerceT, ArgRequired>]>;


/**
Expand Down
55 changes: 49 additions & 6 deletions tests/arguments.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,35 +167,63 @@ program
expectAssignable<OptionValues>(options);
});

program
program
.addArgument(new Argument('[foo...]'))
.action((foo, options) => {
expectType<string[]>(foo);
expectAssignable<OptionValues>(options);
});

program
.addArgument(new Argument('[foo]').default('x'))
.action((foo, options) => {
expectType<string>(foo);
expectAssignable<OptionValues>(options);
});

// historical behaviour, not core
// mixed types possible, but unusual
program
.addArgument(new Argument('<foo>').default(3))
.addArgument(new Argument('[foo]').default(3))
.action((foo, options) => {
expectType<string | number>(foo);
expectAssignable<OptionValues>(options);
});

program
.addArgument(new Argument('<foo>').argOptional())
.addArgument(new Argument('foo'))
.action((foo, options) => {
expectType<string | undefined>(foo);
expectType<string>(foo);
expectAssignable<OptionValues>(options);
});

program
.addArgument(new Argument('[foo]').argRequired())
.addArgument(new Argument('foo').argRequired())
.action((foo, options) => {
expectType<string>(foo);
expectAssignable<OptionValues>(options);
});

program
.addArgument(new Argument('foo').argOptional())
.action((foo, options) => {
expectType<string | undefined>(foo);
expectAssignable<OptionValues>(options);
});

program
.addArgument(new Argument('foo...').argRequired())
.action((foo, options) => {
expectType<string[]>(foo);
expectAssignable<OptionValues>(options);
});

program
.addArgument(new Argument('foo...').argOptional())
.action((foo, options) => {
expectType<string[]>(foo);
expectAssignable<OptionValues>(options);
});

program
.addArgument(new Argument('<foo>').argParser(myParseInt))
.action((foo, options) => {
Expand All @@ -217,6 +245,21 @@ program
expectAssignable<OptionValues>(options);
});

// Test default then optional play well together.
program
.addArgument(new Argument('foo').default('missing').argOptional())
.action((foo, options) => {
expectType<string>(foo);
expectAssignable<OptionValues>(options);
});

// Test optional then default play well together.
program
.addArgument(new Argument('foo').argOptional().default('missing'))
.action((foo, options) => {
expectType<string>(foo);
expectAssignable<OptionValues>(options);
});

/**
* Check command-arguments from .command('name <ARGS>')
Expand Down