diff --git a/src/flags.ts b/src/flags.ts index 5d2cac8dc..277fd0dfd 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -160,7 +160,7 @@ export function option( multiple: true } & ( {required: true} | { - default?: OptionFlag[], P>['default'] | undefined; + default: OptionFlag[], P>['default'] | undefined; } ), ): FlagDefinition diff --git a/src/interfaces/parser.ts b/src/interfaces/parser.ts index f06faeec1..43fc5227c 100644 --- a/src/interfaces/parser.ts +++ b/src/interfaces/parser.ts @@ -271,25 +271,88 @@ type FlagReturnType = T[] | undefined : T | undefined +/** + * FlagDefinition types a function that takes `options` and returns an OptionFlag. + * + * This is returned by `Flags.custom()` and `Flags.option()`, which each take a `defaults` object + * that mirrors the OptionFlag interface. + * + * The `T` in the `OptionFlag` return type is determined by a combination of the provided defaults for + * `multiple`, `required`, and `default` and the provided options for those same properties. If these properties + * are provided in the options, they override the defaults. + * + * no options or defaults -> T | undefined + * `required` -> T + * `default` -> T + * `multiple` -> T[] | undefined + * `required` + `multiple` -> T[] + * `default` + `multiple` -> T[] + */ export type FlagDefinition< T, P = CustomOptions, R extends ReturnTypeSwitches = {multiple: false, requiredOrDefaulted: false} > = { ( - options: P & { multiple?: false | undefined; } & ({ required: true } | { default: OptionFlag['default'] }) & Partial> + // `multiple` is set to false and `required` is set to true in options, potentially overriding the default + options: P & { multiple: false; required: true } & Partial, P>> + ): OptionFlag>; + ( + // `multiple` is set to true and `required` is set to false in options, potentially overriding the default + options: P & { multiple: true; required: false } & Partial, P>> + ): OptionFlag>; + ( + // `multiple` is set to true and `required` is set to false in options, potentially overriding the default + options: P & { multiple: false; required: false } & Partial, P>> + ): OptionFlag>; + ( + options: R['multiple'] extends true ? + // `multiple` is defaulted to true and either `required=true` or `default` are provided in options + P & ( + { required: true } | + { default: OptionFlag, P>['default'] } + ) & Partial, P>> : + // `multiple` is NOT defaulted to true and either `required=true` or `default` are provided in options + P & { multiple?: false | undefined } & ( + { required: true } | + { default: OptionFlag, P>['default'] } + ) & Partial, P>> ): OptionFlag>; ( - options: P & { required: false } & Partial> + options: R['multiple'] extends true ? + // `multiple` is defaulted to true and either `required=true` or `default` are provided in options + P & ( + { required: true } | + { default: OptionFlag, P>['default'] } + ) & Partial, P>> : + // `multiple` is NOT defaulted to true but `multiple=true` and either `required=true` or `default` are provided in options + P & { multiple: true } & ( + { required: true } | + { default: OptionFlag, P>['default'] } + ) & Partial, P>> + ): OptionFlag>; + ( + // `multiple` is not provided in options but either `required=true` or `default` are provided + options: P & { multiple?: false | undefined; } & ( + { required: true } | + { default: OptionFlag, P>['default'] } + ) & Partial, P>> + ): OptionFlag>; + ( + // `required` is set to false in options, potentially overriding the default + options: P & { required: false } & Partial, P>> ): OptionFlag>; ( - options?: P & { multiple?: false | undefined } & Partial> - ): OptionFlag>; + // `multiple` is set to false in options, potentially overriding the default + options: P & { multiple: false } & Partial, P>> + ): OptionFlag>; ( - options: P & { multiple: true } & ({ required: true } | { default: OptionFlag['default'] }) & Partial> - ): OptionFlag>; + // Catch all for when `multiple` is not set in the options + options?: P & { multiple?: false | undefined } & Partial, P>> + ): OptionFlag>; ( - options: P & { multiple: true } & Partial> + // `multiple` is set to true in options, potentially overriding the default + options: P & { multiple: true } & Partial, P>> ): OptionFlag>; } diff --git a/test/interfaces/flags.test-types.ts b/test/interfaces/flags.test-types.ts index dba6c7dc5..867d301c3 100644 --- a/test/interfaces/flags.test-types.ts +++ b/test/interfaces/flags.test-types.ts @@ -39,51 +39,56 @@ export const arrayFlag = Flags.custom({ const options = ['foo', 'bar'] as const -const errorMultipleTrueDefaultSingleOption = Flags.option({ +Flags.option({ options, multiple: true, // @ts-expect-error because multiple is true, default must be an array default: 'foo', }) -errorMultipleTrueDefaultSingleOption({ +Flags.option({options})({ multiple: true, // @ts-expect-error because multiple is true, default must be an array default: 'foo', }) // @ts-expect-error because multiple is false, default must be a single value -export const errorMultipleFalseDefaultArrayOption = Flags.option({ +Flags.option({ options, default: ['foo'], }) // @ts-expect-error because multiple is false, default must be a single value -errorMultipleFalseDefaultArrayOption({ +Flags.option({options, multiple: false})({ default: ['foo'], }) -const errorMultipleTrueDefaultSingleCustom = Flags.custom({ +Flags.custom({ options, multiple: true, // @ts-expect-error because multiple is true, default must be an array default: 'foo', }) -errorMultipleTrueDefaultSingleCustom({ +Flags.custom()({ multiple: true, // @ts-expect-error because multiple is true, default must be an array default: 'foo', }) +Flags.custom({multiple: true})({ + // @ts-expect-error because multiple is true, default must be an array + default: 'foo', +}) + // @ts-expect-error because multiple is false, default must be a single value -const errorMultipleFalseDefaultArrayCustom = Flags.custom({ +Flags.custom({ options, default: ['foo'], }) // @ts-expect-error because multiple is false, default must be a single value -errorMultipleFalseDefaultArrayCustom({ +Flags.custom({multiple: false})({ default: ['foo'], }) @@ -241,8 +246,21 @@ class MyCommand extends BaseCommand { options, multiple: true, })({ - // TODO: fix this - // default: async _ctx => ['foo'], + default: ['foo'], + }), + + 'option#defs:multiple;opts:default-callback': Flags.option({ + options, + multiple: true, + })({ + default: async _ctx => ['foo'], + }), + + 'custom#defs:multiple;opts:default-callback': Flags.custom({ + options, + multiple: true, + })({ + default: async _ctx => ['foo'], }), 'custom#defs:multiple,parse': Flags.custom({ @@ -250,12 +268,74 @@ class MyCommand extends BaseCommand { parse: async (input, _ctx, _opts) => input, })(), - // TODO: fix this - // 'option#defs:multiple,prase': Flags.option({ - // options, - // multiple: true, - // parse: async (input, _ctx, _opts) => input, - // })(), + 'option#defs:multiple,prase': Flags.option({ + options, + multiple: true, + parse: async (input, _ctx, _opts) => input as typeof options[number], + })(), + + 'custom#defs:multiple=true;opts:multiple=false': Flags.custom({ + multiple: true, + })({ + multiple: false, + }), + 'custom#defs:multiple=false;opts:multiple=true': Flags.custom({ + multiple: false, + })({ + multiple: true, + }), + 'custom#defs:required=true;opts:required=false': Flags.custom({ + required: true, + })({ + required: false, + }), + 'custom#defs:required=false;opts:required=true': Flags.custom({ + required: false, + })({ + required: true, + }), + 'custom#defs:multiple=true;opts:multiple=false,required=true': Flags.custom({ + multiple: true, + })({ + multiple: false, + required: true, + }), + 'custom#defs:required=true;opts:multiple=true,required=false': Flags.custom({ + required: true, + })({ + multiple: true, + required: false, + }), + 'custom#defs:required=false;opts:multiple=true,required=true': Flags.custom({ + required: false, + })({ + multiple: true, + required: true, + }), + + 'custom#defs:multiple=true,required=true;opts:multiple=false,required=false': Flags.custom({ + multiple: true, + required: true, + })({ + multiple: false, + required: false, + }), + + 'custom#defs:multiple=false,required=false;opts:multiple=true,required=true': Flags.custom({ + multiple: false, + required: false, + })({ + multiple: true, + required: true, + }), + + 'custom#defs:multiple=true;opts:multiple=false,default': Flags.custom({ + multiple: true, + })({ + multiple: false, + // TODO: THIS IS A BUG. It should enforce a single value instead of allowing a single value or an array + default: ['foo'], + }), } public static '--' = true @@ -400,6 +480,35 @@ class MyCommand extends BaseCommand { expectType(this.flags['option#defs,multiple,default']) expectNotType(this.flags['option#defs,multiple,default']) + expectType(this.flags['option#defs:multiple;opts:default']) + expectNotType(this.flags['option#defs:multiple;opts:default']) + + expectType(this.flags['option#defs:multiple;opts:default-callback']) + expectNotType(this.flags['option#defs:multiple;opts:default-callback']) + + expectType(this.flags['custom#defs:multiple;opts:default-callback']) + + expectType(this.flags['custom#defs:multiple,parse']) + + expectType<(typeof options[number])[] | undefined>(this.flags['option#defs:multiple,prase']) + + expectType(this.flags['custom#defs:multiple=true;opts:multiple=false']) + expectType(this.flags['custom#defs:multiple=false;opts:multiple=true']) + expectType(this.flags['custom#defs:required=true;opts:required=false']) + expectType(this.flags['custom#defs:required=false;opts:required=true']) + expectNotType(this.flags['custom#defs:required=false;opts:required=true']) + expectType(this.flags['custom#defs:multiple=true;opts:multiple=false,required=true']) + expectNotType(this.flags['custom#defs:multiple=true;opts:multiple=false,required=true']) + expectType(this.flags['custom#defs:required=true;opts:multiple=true,required=false']) + expectType(this.flags['custom#defs:required=false;opts:multiple=true,required=true']) + expectNotType(this.flags['custom#defs:required=false;opts:multiple=true,required=true']) + expectType(this.flags['custom#defs:multiple=true,required=true;opts:multiple=false,required=false']) + expectType(this.flags['custom#defs:multiple=false,required=false;opts:multiple=true,required=true']) + expectNotType(this.flags['custom#defs:multiple=false,required=false;opts:multiple=true,required=true']) + + // TODO: Known issue with `default` not enforcing the correct type whenever multiple is defaulted to true but then overridden to false + // expectType(this.flags['custom#defs:multiple=true;opts:multiple=false,default']) + return result.flags } }