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

feat: add InferredFlags type #473

Merged
merged 11 commits into from Aug 23, 2022
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -74,6 +74,7 @@
"shx": "^0.3.4",
"sinon": "^11.1.2",
"ts-node": "^9.1.1",
"tsd": "^0.22.0",
"typescript": "4.5.5"
},
"engines": {
Expand Down
29 changes: 8 additions & 21 deletions src/flags.ts
@@ -1,23 +1,14 @@
import {OptionFlag, Definition, BooleanFlag, EnumFlagOptions, Default} from './interfaces'
import * as Parser from './parser'
import {OptionFlag, BooleanFlag, EnumFlagOptions, Default} from './interfaces'
import {custom, boolean} from './parser'
import Command from './command'
export {boolean, integer, url, directory, file, string, build, option, custom} from './parser'

export function build<T>(defaults: {parse: OptionFlag<T>['parse']} & Partial<OptionFlag<T>>): Definition<T>
export function build(defaults: Partial<OptionFlag<string>>): Definition<string>
export function build<T>(defaults: Partial<OptionFlag<T>>): Definition<T> {
return Parser.flags.build<T>(defaults as any)
}

export function option<T>(options: {parse: OptionFlag<T>['parse']} & Partial<OptionFlag<T>>) {
return build<T>(options)()
}

export function _enum<T = string>(opts: EnumFlagOptions<T> & {multiple: true} & ({required: true} | { default: Default<T> })): OptionFlag<T[]>
export function _enum<T = string>(opts: EnumFlagOptions<T> & {multiple: true}): OptionFlag<T[] | undefined>
export function _enum<T = string>(opts: EnumFlagOptions<T, true> & {multiple: true} & ({required: true} | { default: Default<T[]> })): OptionFlag<T[]>
export function _enum<T = string>(opts: EnumFlagOptions<T, true> & {multiple: true}): OptionFlag<T[] | undefined>
export function _enum<T = string>(opts: EnumFlagOptions<T> & ({required: true} | { default: Default<T> })): OptionFlag<T>
export function _enum<T = string>(opts: EnumFlagOptions<T>): OptionFlag<T | undefined>
export function _enum<T = string>(opts: EnumFlagOptions<T>): OptionFlag<T> | OptionFlag<T[]> | OptionFlag<T | undefined> | OptionFlag<T[] | undefined> {
return build<T>({
return custom<T, EnumFlagOptions<T>>({
async parse(input) {
if (!opts.options.includes(input)) throw new Error(`Expected --${this.name}=${input} to be one of: ${opts.options.join(', ')}`)
return input as unknown as T
Expand All @@ -29,12 +20,8 @@ export function _enum<T = string>(opts: EnumFlagOptions<T>): OptionFlag<T> | Opt

export {_enum as enum}

const stringFlag = build({})
export {stringFlag as string}
export {boolean, integer, url, directory, file} from './parser'

export const version = (opts: Partial<BooleanFlag<boolean>> = {}) => {
return Parser.flags.boolean({
return boolean({
description: 'Show CLI version.',
...opts,
parse: async (_: any, cmd: Command) => {
Expand All @@ -45,7 +32,7 @@ export const version = (opts: Partial<BooleanFlag<boolean>> = {}) => {
}

export const help = (opts: Partial<BooleanFlag<boolean>> = {}) => {
return Parser.flags.boolean({
return boolean({
description: 'Show CLI help.',
...opts,
parse: async (_: any, cmd: Command) => {
Expand Down
33 changes: 33 additions & 0 deletions src/interfaces/flags.ts
@@ -0,0 +1,33 @@
import {FlagInput} from './parser'

/**
* Infer the flags that are returned by Command.parse. This is useful for when you want to assign the flags as a class property.
*
* @example
* export type StatusFlags = Interfaces.InferredFlags<typeof Status.flags & typeof Status.globalFlags>
*
* export abstract class BaseCommand extends Command {
* static enableJsonFlag = true
*
* static globalFlags = {
* config: Flags.string({
* description: 'specify config file',
* }),
* }
* }
*
* export default class Status extends BaseCommand {
* static flags = {
* force: Flags.boolean({char: 'f', description: 'a flag'}),
* }
*
* public flags!: StatusFlags
*
* public async run(): Promise<StatusFlags> {
* const result = await this.parse(Status)
* this.flags = result.flags
* return result.flags
* }
* }
*/
export type InferredFlags<T> = T extends FlagInput<infer F> ? F & { json: boolean | undefined; } : unknown
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any way to test this? Maybe by building a test class and having a flags property like your example, and then asserting things about its type?

What I'd like to do is prevent some future flag changes/refactoring/type fixing from breaking this feature.

1 change: 1 addition & 0 deletions src/interfaces/index.ts
Expand Up @@ -19,3 +19,4 @@ export {PJSON} from './pjson'
export {Plugin, PluginOptions, Options} from './plugin'
export {Topic} from './topic'
export {TSConfig} from './ts-config'
export {InferredFlags} from './flags'
85 changes: 58 additions & 27 deletions src/interfaces/parser.ts
Expand Up @@ -78,13 +78,13 @@ type MetadataFlag = {
export type ListItem = [string, string | undefined]
export type List = ListItem[]

export type DefaultContext<T> = {
options: OptionFlag<T>;
flags: { [k: string]: string };
export type DefaultContext<T, P> = {
options: P & OptionFlag<T>;
flags: Record<string, string>;
}

export type Default<T> = T | ((context: DefaultContext<T>) => Promise<T>)
export type DefaultHelp<T> = T | ((context: DefaultContext<T>) => Promise<string | undefined>)
export type Default<T, P = Record<string, unknown>> = T | ((context: DefaultContext<T, P>) => Promise<T>)
export type DefaultHelp<T, P = Record<string, unknown>> = T | ((context: DefaultContext<T, P>) => Promise<string | undefined>)

export type FlagProps = {
name: string;
Expand All @@ -109,10 +109,30 @@ export type FlagProps = {
* Shows this flag in a separate list in the help.
*/
helpGroup?: string;
/**
* Accept an environment variable as input
*/
env?: string;
/**
* If true, the flag will not be shown in the help.
*/
hidden?: boolean;
/**
* If true, the flag will be required.
*/
required?: boolean;
/**
* List of flags that this flag depends on.
*/
dependsOn?: string[];
/**
* List of flags that cannot be used with this flag.
*/
exclusive?: string[];
/**
* Exactly one of these flags must be provided.
*/
exactlyOne?: string[];
}

export type BooleanFlagProps = FlagProps & {
Expand All @@ -124,46 +144,57 @@ export type OptionFlagProps = FlagProps & {
type: 'option';
helpValue?: string;
options?: string[];
multiple: boolean;
multiple?: boolean;
}

export type FlagBase<T, I> = FlagProps & {
exactlyOne?: string[];
/**
* also accept an environment variable as input
*/
env?: string;
parse(input: I, context: any): Promise<T>;
export type FlagParser<T, I, P = any> = (input: I, context: any, opts: P & OptionFlag<T>) => Promise<T>

export type FlagBase<T, I, P = any> = FlagProps & {
parse: FlagParser<T, I, P>;
}

export type BooleanFlag<T> = FlagBase<T, boolean> & BooleanFlagProps & {
/**
* specifying a default of false is the same not specifying a default
* specifying a default of false is the same as not specifying a default
*/
default?: Default<boolean>;
}
export type OptionFlag<T> = FlagBase<T, string> & OptionFlagProps & {
default?: Default<T | undefined>;

export type CustomOptionFlag<T, P = any, M = false> = FlagBase<T, string, P> & OptionFlagProps & {
defaultHelp?: DefaultHelp<T>;
input: string[];
default?: M extends true ? Default<T[] | undefined, P> : Default<T | undefined, P>;
}

export type Definition<T> = {
export type OptionFlag<T> = FlagBase<T, string> & OptionFlagProps & {
defaultHelp?: DefaultHelp<T>;
input: string[];
} & ({
default?: Default<T | undefined>;
multiple: false;
} | {
default?: Default<T[] | undefined>;
multiple: true;
})

export type Definition<T, P = Record<string, unknown>> = {
(
options: { multiple: true } & ({ required: true } | { default: Default<T> }) &
Partial<OptionFlag<T>>,
options: P & { multiple: true } & ({ required: true } | { default: Default<T[]> }) & Partial<OptionFlag<T>>
): OptionFlag<T[]>;
(options: { multiple: true } & Partial<OptionFlag<T[]>>): OptionFlag<T[] | undefined>;
(
options: ({ required: true } | { default: Default<T> }) &
Partial<OptionFlag<T>>,
): OptionFlag<T>;
(options?: Partial<OptionFlag<T>>): OptionFlag<T | undefined>;
(options: P & { multiple: true } & Partial<OptionFlag<T>>): OptionFlag<T[] | undefined>;
(options: P & ({ required: true } | { default: Default<T> }) & Partial<OptionFlag<T>>): OptionFlag<T>;
(options?: P & Partial<OptionFlag<T>>): OptionFlag<T | undefined>;
}

export type EnumFlagOptions<T> = Partial<OptionFlag<T>> & {
export type EnumFlagOptions<T, M = false> = Partial<CustomOptionFlag<T, any, M>> & {
options: T[];
}
} & ({
default?: Default<T | undefined>;
multiple?: false;
} | {
default?: Default<T[] | undefined>;
multiple: true;
})

export type Flag<T> = BooleanFlag<T> | OptionFlag<T>

Expand Down