Skip to content

Commit

Permalink
feat: final flag types bugs and more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonnalley committed Sep 14, 2023
1 parent ea7f431 commit 3c4e0df
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 24 deletions.
2 changes: 1 addition & 1 deletion src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export function option<T extends readonly string[], P extends CustomOptions>(
multiple: true
} & (
{required: true} | {
default?: OptionFlag<ElementType<T>[], P>['default'] | undefined;
default: OptionFlag<ElementType<T>[], P>['default'] | undefined;
}
),
): FlagDefinition<typeof defaults.options[number], P, {multiple: true; requiredOrDefaulted: true}>
Expand Down
77 changes: 70 additions & 7 deletions src/interfaces/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,25 +271,88 @@ type FlagReturnType<T, R extends ReturnTypeSwitches> =
T[] | undefined :
T | undefined

/**
* FlagDefinition types a function that takes `options` and returns an OptionFlag<T>.
*
* 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<T>` 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<T, P>['default'] }) & Partial<OptionFlag<T, P>>
// `multiple` is set to false and `required` is set to true in options, potentially overriding the default
options: P & { multiple: false; required: true } & Partial<OptionFlag<FlagReturnType<T, {multiple: false, requiredOrDefaulted: true}>, P>>
): OptionFlag<FlagReturnType<T, {multiple: false, requiredOrDefaulted: true}>>;
(
// `multiple` is set to true and `required` is set to false in options, potentially overriding the default
options: P & { multiple: true; required: false } & Partial<OptionFlag<FlagReturnType<T, {multiple: true, requiredOrDefaulted: false}>, P>>
): OptionFlag<FlagReturnType<T, {multiple: true, requiredOrDefaulted: false}>>;
(
// `multiple` is set to true and `required` is set to false in options, potentially overriding the default
options: P & { multiple: false; required: false } & Partial<OptionFlag<FlagReturnType<T, {multiple: false, requiredOrDefaulted: false}>, P>>
): OptionFlag<FlagReturnType<T, {multiple: false, requiredOrDefaulted: false}>>;
(
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<FlagReturnType<T, {multiple: R['multiple']; requiredOrDefaulted: true}>, P>['default'] }
) & Partial<OptionFlag<FlagReturnType<T, {multiple: R['multiple']; requiredOrDefaulted: true}>, 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<FlagReturnType<T, {multiple: R['multiple']; requiredOrDefaulted: true}>, P>['default'] }
) & Partial<OptionFlag<FlagReturnType<T, {multiple: R['multiple']; requiredOrDefaulted: true}>, P>>
): OptionFlag<FlagReturnType<T, {multiple: R['multiple']; requiredOrDefaulted: true}>>;
(
options: P & { required: false } & Partial<OptionFlag<T, P>>
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<FlagReturnType<T, {multiple: true; requiredOrDefaulted: true}>, P>['default'] }
) & Partial<OptionFlag<FlagReturnType<T, {multiple: true; requiredOrDefaulted: true}>, 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<FlagReturnType<T, {multiple: true; requiredOrDefaulted: true}>, P>['default'] }
) & Partial<OptionFlag<FlagReturnType<T, {multiple: true; requiredOrDefaulted: true}>, P>>
): OptionFlag<FlagReturnType<T, {multiple: true; requiredOrDefaulted: true}>>;
(
// `multiple` is not provided in options but either `required=true` or `default` are provided
options: P & { multiple?: false | undefined; } & (
{ required: true } |
{ default: OptionFlag<FlagReturnType<T, {multiple: R['multiple']; requiredOrDefaulted: true}>, P>['default'] }
) & Partial<OptionFlag<FlagReturnType<T, {multiple: R['multiple']; requiredOrDefaulted: true}>, P>>
): OptionFlag<FlagReturnType<T, {multiple: R['multiple']; requiredOrDefaulted: true}>>;
(
// `required` is set to false in options, potentially overriding the default
options: P & { required: false } & Partial<OptionFlag<FlagReturnType<T, {multiple: R['multiple']; requiredOrDefaulted: false}>, P>>
): OptionFlag<FlagReturnType<T, {multiple: R['multiple']; requiredOrDefaulted: false}>>;
(
options?: P & { multiple?: false | undefined } & Partial<OptionFlag<T, P>>
): OptionFlag<FlagReturnType<T, R>>;
// `multiple` is set to false in options, potentially overriding the default
options: P & { multiple: false } & Partial<OptionFlag<FlagReturnType<T, {multiple: false, requiredOrDefaulted: R['requiredOrDefaulted']}>, P>>
): OptionFlag<FlagReturnType<T, {multiple: false, requiredOrDefaulted: R['requiredOrDefaulted']}>>;
(
options: P & { multiple: true } & ({ required: true } | { default: OptionFlag<T[], P>['default'] }) & Partial<OptionFlag<T[], P>>
): OptionFlag<FlagReturnType<T, {multiple: true; requiredOrDefaulted: true}>>;
// Catch all for when `multiple` is not set in the options
options?: P & { multiple?: false | undefined } & Partial<OptionFlag<FlagReturnType<T, R>, P>>
): OptionFlag<FlagReturnType<T, R>>;
(
options: P & { multiple: true } & Partial<OptionFlag<T[], P>>
// `multiple` is set to true in options, potentially overriding the default
options: P & { multiple: true } & Partial<OptionFlag<FlagReturnType<T, {multiple: true, requiredOrDefaulted: R['requiredOrDefaulted']}>, P>>
): OptionFlag<FlagReturnType<T, {multiple: true, requiredOrDefaulted: R['requiredOrDefaulted']}>>;
}

Expand Down
141 changes: 125 additions & 16 deletions test/interfaces/flags.test-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,51 +39,56 @@ export const arrayFlag = Flags.custom<string[]>({

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'],
})

Expand Down Expand Up @@ -241,21 +246,96 @@ 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({
multiple: true,
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

Check warning on line 336 in test/interfaces/flags.test-types.ts

View workflow job for this annotation

GitHub Actions / linux-unit-tests / linux-unit-tests (lts/-1)

Unexpected 'todo' comment: 'TODO: THIS IS A BUG. It should enforce a...'

Check warning on line 336 in test/interfaces/flags.test-types.ts

View workflow job for this annotation

GitHub Actions / linux-unit-tests / linux-unit-tests (lts/*)

Unexpected 'todo' comment: 'TODO: THIS IS A BUG. It should enforce a...'

Check warning on line 336 in test/interfaces/flags.test-types.ts

View workflow job for this annotation

GitHub Actions / linux-unit-tests / linux-unit-tests (latest)

Unexpected 'todo' comment: 'TODO: THIS IS A BUG. It should enforce a...'

Check warning on line 336 in test/interfaces/flags.test-types.ts

View workflow job for this annotation

GitHub Actions / windows-unit-tests / windows-unit-tests (lts/-1)

Unexpected 'todo' comment: 'TODO: THIS IS A BUG. It should enforce a...'

Check warning on line 336 in test/interfaces/flags.test-types.ts

View workflow job for this annotation

GitHub Actions / windows-unit-tests / windows-unit-tests (lts/*)

Unexpected 'todo' comment: 'TODO: THIS IS A BUG. It should enforce a...'

Check warning on line 336 in test/interfaces/flags.test-types.ts

View workflow job for this annotation

GitHub Actions / windows-unit-tests / windows-unit-tests (latest)

Unexpected 'todo' comment: 'TODO: THIS IS A BUG. It should enforce a...'
default: ['foo'],
}),
}

public static '--' = true
Expand Down Expand Up @@ -400,6 +480,35 @@ class MyCommand extends BaseCommand {
expectType<string[]>(this.flags['option#defs,multiple,default'])
expectNotType<undefined>(this.flags['option#defs,multiple,default'])

expectType<string[]>(this.flags['option#defs:multiple;opts:default'])
expectNotType<undefined>(this.flags['option#defs:multiple;opts:default'])

expectType<string[]>(this.flags['option#defs:multiple;opts:default-callback'])
expectNotType<undefined>(this.flags['option#defs:multiple;opts:default-callback'])

expectType<string[]>(this.flags['custom#defs:multiple;opts:default-callback'])

expectType<string[] | undefined>(this.flags['custom#defs:multiple,parse'])

expectType<(typeof options[number])[] | undefined>(this.flags['option#defs:multiple,prase'])

expectType<string | undefined>(this.flags['custom#defs:multiple=true;opts:multiple=false'])
expectType<string[] | undefined>(this.flags['custom#defs:multiple=false;opts:multiple=true'])
expectType<string | undefined>(this.flags['custom#defs:required=true;opts:required=false'])
expectType<string>(this.flags['custom#defs:required=false;opts:required=true'])
expectNotType<undefined>(this.flags['custom#defs:required=false;opts:required=true'])
expectType<string>(this.flags['custom#defs:multiple=true;opts:multiple=false,required=true'])
expectNotType<undefined>(this.flags['custom#defs:multiple=true;opts:multiple=false,required=true'])
expectType<string[] | undefined>(this.flags['custom#defs:required=true;opts:multiple=true,required=false'])
expectType<string[]>(this.flags['custom#defs:required=false;opts:multiple=true,required=true'])
expectNotType<undefined>(this.flags['custom#defs:required=false;opts:multiple=true,required=true'])
expectType<string | undefined>(this.flags['custom#defs:multiple=true,required=true;opts:multiple=false,required=false'])
expectType<string[]>(this.flags['custom#defs:multiple=false,required=false;opts:multiple=true,required=true'])
expectNotType<undefined>(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

Check warning on line 509 in test/interfaces/flags.test-types.ts

View workflow job for this annotation

GitHub Actions / linux-unit-tests / linux-unit-tests (lts/-1)

Unexpected 'todo' comment: 'TODO: Known issue with `default` not...'

Check warning on line 509 in test/interfaces/flags.test-types.ts

View workflow job for this annotation

GitHub Actions / linux-unit-tests / linux-unit-tests (lts/*)

Unexpected 'todo' comment: 'TODO: Known issue with `default` not...'

Check warning on line 509 in test/interfaces/flags.test-types.ts

View workflow job for this annotation

GitHub Actions / linux-unit-tests / linux-unit-tests (latest)

Unexpected 'todo' comment: 'TODO: Known issue with `default` not...'

Check warning on line 509 in test/interfaces/flags.test-types.ts

View workflow job for this annotation

GitHub Actions / windows-unit-tests / windows-unit-tests (lts/-1)

Unexpected 'todo' comment: 'TODO: Known issue with `default` not...'

Check warning on line 509 in test/interfaces/flags.test-types.ts

View workflow job for this annotation

GitHub Actions / windows-unit-tests / windows-unit-tests (lts/*)

Unexpected 'todo' comment: 'TODO: Known issue with `default` not...'

Check warning on line 509 in test/interfaces/flags.test-types.ts

View workflow job for this annotation

GitHub Actions / windows-unit-tests / windows-unit-tests (latest)

Unexpected 'todo' comment: 'TODO: Known issue with `default` not...'
// expectType<string>(this.flags['custom#defs:multiple=true;opts:multiple=false,default'])

return result.flags
}
}
Expand Down

0 comments on commit 3c4e0df

Please sign in to comment.