diff --git a/src/args.ts b/src/args.ts index f992da5..d161ee9 100644 --- a/src/args.ts +++ b/src/args.ts @@ -1,8 +1,15 @@ +import {IArgsList} from './parse' + export type ParseFn = (input: string) => T +export type NameOf = + T extends {name: string} ? T['name'] : + T extends Arg ? N : + never; + // eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface IArg { - name: string; +export interface IArg { + name: N; description?: string; required?: boolean; hidden?: boolean; @@ -11,8 +18,8 @@ export interface IArg { options?: string[]; } -export interface ArgBase { - name?: string; +export interface ArgBase { + name?: N; description?: string; hidden?: boolean; parse: ParseFn; @@ -21,27 +28,26 @@ export interface ArgBase { options?: string[]; } -export type RequiredArg = ArgBase & { +export type RequiredArg = ArgBase & { required: true; value: T; } -export type OptionalArg = ArgBase & { +export type OptionalArg = ArgBase & { required: false; value?: T; } -export type Arg = RequiredArg | OptionalArg +export type Arg = RequiredArg | OptionalArg -export function newArg(arg: IArg & { Parse: ParseFn }): Arg -export function newArg(arg: IArg): Arg -export function newArg(arg: IArg): any { +export function newArg(arg: IArg & { Parse: ParseFn }): Arg, T> +export function newArg(arg: IArg): Arg, string> +export function newArg(arg: IArg): Arg, any> { return { parse: (i: string) => i, ...arg, required: Boolean(arg.required), - } + } as any } -export interface Output {[name: string]: any} -export type Input = IArg[] +export type Input = IArgsList; diff --git a/src/errors.ts b/src/errors.ts index dfaf1f5..19c7723 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -20,7 +20,7 @@ const m = Deps() export interface ICLIParseErrorOptions { parse: { input?: ParserInput; - output?: ParserOutput; + output?: ParserOutput; }; } @@ -35,9 +35,9 @@ export class CLIParseError extends CLIError { } export class InvalidArgsSpecError extends CLIParseError { - public args: Arg[] + public args: Arg[] - constructor({args, parse}: ICLIParseErrorOptions & { args: Arg[] }) { + constructor({args, parse}: ICLIParseErrorOptions & { args: Arg[] }) { let message = 'Invalid argument spec' const namedArgs = args.filter(a => a.name) if (namedArgs.length > 0) { @@ -50,9 +50,9 @@ export class InvalidArgsSpecError extends CLIParseError { } export class RequiredArgsError extends CLIParseError { - public args: Arg[] + public args: Arg[] - constructor({args, parse}: ICLIParseErrorOptions & { args: Arg[] }) { + constructor({args, parse}: ICLIParseErrorOptions & { args: Arg[] }) { let message = `Missing ${args.length} required arg${args.length === 1 ? '' : 's'}` const namedArgs = args.filter(a => a.name) if (namedArgs.length > 0) { @@ -93,7 +93,7 @@ export class FlagInvalidOptionError extends CLIParseError { } export class ArgInvalidOptionError extends CLIParseError { - constructor(arg: Arg, input: string) { + constructor(arg: Arg, input: string) { const message = `Expected ${input} to be one of: ${arg.options!.join(', ')}` super({parse: {}, message}) } diff --git a/src/index.ts b/src/index.ts index 329511b..2453ed0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,13 @@ import * as args from './args' import Deps from './deps' import * as flags from './flags' -import {OutputArgs, OutputFlags, Parser, ParserOutput as Output} from './parse' +import { + OutputArgs, + OutputFlags, + Parser, + ParserRawInput as Input, + ParserOutput as Output, +} from './parse' import * as Validate from './validate' export {args} export {flags} @@ -14,19 +20,16 @@ const m = Deps() // eslint-disable-next-line node/no-missing-require .add('validate', () => require('./validate').validate as typeof Validate.validate) -export type Input = { - flags?: flags.Input; - args?: args.Input; - strict?: boolean; - context?: any; - '--'?: boolean; -} +export type FlagsOf['flags'] | undefined> = + T extends undefined ? undefined : + T extends Input ? OutputFlags : + never; -export function parse(argv: string[], options: Input): Output { +export function parse>(argv: string[], options: TInput): Output { const input = { argv, context: options.context, - args: (options.args || []).map((a: any) => args.newArg(a as any)), + args: (options.args || []).map(a => args.newArg(a)), '--': options['--'], flags: { color: flags.defaultFlags.color, diff --git a/src/parse.ts b/src/parse.ts index 7619702..13d2d32 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -1,11 +1,12 @@ // tslint:disable interface-over-type-literal -import {Arg} from './args' +import {Arg, IArg, NameOf} from './args' import Deps from './deps' import * as Errors from './errors' import * as Flags from './flags' import {Metadata} from './metadata' import * as Util from './util' +import {flags, args} from '.' // eslint-disable-next-line new-cap const m = Deps() @@ -25,11 +26,44 @@ try { debug = () => {} } -export type OutputArgs = { [P in keyof T]: any } -export type OutputFlags = { [P in keyof T]: any } -export type ParserOutput, TArgs extends OutputArgs> = { - flags: TFlags; - args: TArgs; +type DefaultOf = T extends { default: () => infer Default } ? Default + : T extends { default: infer Default } ? Default + : never; +type RequiredTypeOf> = T extends { + name: N; + parse: (raw: string) => infer R; +} + ? {[key in N]: R | DefaultOf} + : {[key in N]: string | DefaultOf}; + +type TypeOf = T extends {required: true} + ? RequiredTypeOf + : Partial>; + +type TypesOf = { + [I in keyof Args]: TypeOf +}[number]; + +type UnionToIntersection = + (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) + ? I + : never; + +export type ParserRawInput = { + flags?: flags.Input; + args?: args.Input; + strict?: boolean; + context?: any; + '--'?: boolean; +}; + +export type OutputArgs['args']> = T extends ParserRawInput['args'] ? + UnionToIntersection>> : + undefined; +export type OutputFlags = { [P in keyof NonNullable]: any } +export type ParserOutput> = { + flags: OutputFlags; + args: OutputArgs; argv: string[]; raw: ParsingToken[]; metadata: Metadata; @@ -39,16 +73,21 @@ export type ArgToken = { type: 'arg'; input: string } export type FlagToken = { type: 'flag'; flag: string; input: string } export type ParsingToken = ArgToken | FlagToken +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface IArgsList extends Array> { + [index: number]: IArg; +} + export interface ParserInput { argv: string[]; flags: Flags.Input; - args: Arg[]; + args: Arg[]; strict: boolean; context: any; '--'?: boolean; } -export class Parser, TArgs extends OutputArgs> { +export class Parser, TArgs extends any> { private readonly argv: string[] private readonly raw: ParsingToken[] = [] diff --git a/src/validate.ts b/src/validate.ts index d5156fd..082085e 100644 --- a/src/validate.ts +++ b/src/validate.ts @@ -4,7 +4,7 @@ import {Arg} from './args' import {InvalidArgsSpecError, RequiredArgsError, RequiredFlagError, UnexpectedArgsError} from './errors' import {ParserInput, ParserOutput} from './parse' -export function validate(parse: { input: ParserInput; output: ParserOutput }) { +export function validate(parse: { input: ParserInput; output: ParserOutput }) { function validateArgs() { const maxArgs = parse.input.args.length if (parse.input.strict && parse.output.argv.length > maxArgs) { @@ -12,7 +12,7 @@ export function validate(parse: { input: ParserInput; output: ParserOutput[] = [] + const missingRequiredArgs: Arg[] = [] let hasOptional = false parse.input.args.forEach((arg, index) => { diff --git a/test/parse.test.ts b/test/parse.test.ts index 8c30995..6ade20c 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -444,7 +444,7 @@ See more help with --help`) it('accepts falsy', () => { const out = parse([], { - args: [{name: 'baz', default: false}], + args: [{name: 'baz' as const, default: false}], }) expect(out.args).to.deep.include({baz: false}) }) @@ -578,7 +578,7 @@ See more help with --help`) describe('arg options', () => { it('accepts valid option', () => { const out = parse(['myotheropt'], { - args: [{name: 'foo', options: ['myopt', 'myotheropt']}], + args: [{name: 'foo' as const, options: ['myopt', 'myotheropt']}], }) expect(out.args.foo).to.equal('myotheropt') })