Skip to content
This repository was archived by the owner on Aug 22, 2023. It is now read-only.

Commit 967cd55

Browse files
committed
fix: add typing to flag output
1 parent 1fc929f commit 967cd55

File tree

7 files changed

+79
-44
lines changed

7 files changed

+79
-44
lines changed

src/args.ts

+1
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ export function newArg(arg: IArg<any>): any {
4040
}
4141
}
4242

43+
export interface Output {[name: string]: any}
4344
export type Input = IArg<any>[]

src/errors.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {ParserInput, ParserOutput} from './parse'
77
export interface ICLIParseErrorOptions {
88
parse: {
99
input: ParserInput
10-
output: ParserOutput
10+
output: ParserOutput<any, any>
1111
}
1212
}
1313

src/flags.ts

+28-16
Original file line numberDiff line numberDiff line change
@@ -2,64 +2,76 @@ import {AlphabetLowercase, AlphabetUppercase} from './alphabet'
22

33
export interface DefaultContext<T> { options: IOptionFlag<T>; flags: { [k: string]: string } }
44

5-
export interface IFlagBase {
5+
export interface IFlagBase<T, I> {
66
name: string
77
char?: AlphabetLowercase | AlphabetUppercase
88
description?: string
99
hidden?: boolean
1010
required?: boolean
11+
parse(input: I): T
1112
}
1213

13-
export interface IBooleanFlag extends IFlagBase {
14+
export interface IBooleanFlag<T> extends IFlagBase<T, boolean> {
1415
type: 'boolean'
1516
allowNo: boolean
1617
}
1718

18-
export interface IOptionFlag<T = string> extends IFlagBase {
19+
export interface IOptionFlag<T> extends IFlagBase<T, string> {
1920
type: 'option'
2021
default?: T | ((context: DefaultContext<T>) => T | undefined)
2122
multiple: boolean
22-
parse(input: string): T
23+
input: string[]
2324
}
2425

25-
export type Definition<T> = (options?: Partial<IOptionFlag<T>>) => IOptionFlag<T>
26+
export interface Definition<T> {
27+
(options: {multiple: true} & Partial<IOptionFlag<T>>): IOptionFlag<T[]>
28+
(options: {required: true} & Partial<IOptionFlag<T>>): IOptionFlag<T>
29+
(options?: Partial<IOptionFlag<T>>): IOptionFlag<T | undefined>
30+
}
2631

27-
export function option<T = string>(defaults: Partial<IOptionFlag<T>> = {}): Definition<T> {
28-
return (options?: any): any => {
29-
options = options || {}
32+
export function build<T>(defaults: {parse: IOptionFlag<T>['parse']} & Partial<IOptionFlag<T>>): Definition<T>
33+
export function build(defaults: Partial<IOptionFlag<string>>): Definition<string>
34+
export function build<T>(defaults: Partial<IOptionFlag<T>>): Definition<T> {
35+
return (options: any = {}): any => {
3036
return {
3137
parse: (i: string) => i,
3238
...defaults,
3339
...options,
34-
input: [],
40+
input: [] as string[],
3541
multiple: !!options.multiple,
3642
type: 'option',
37-
}
43+
} as any
3844
}
3945
}
4046

41-
export type IFlag<T> = IBooleanFlag | IOptionFlag<T>
47+
export type IFlag<T> = IBooleanFlag<T> | IOptionFlag<T>
4248

43-
export function boolean(options: Partial<IBooleanFlag> = {}): IBooleanFlag {
49+
export function boolean<T = boolean>(options: Partial<IBooleanFlag<T>> = {}): IBooleanFlag<T> {
4450
return {
51+
parse: b => b,
4552
...options,
4653
allowNo: !!options.allowNo,
4754
type: 'boolean',
48-
} as IBooleanFlag
55+
} as IBooleanFlag<T>
4956
}
5057

51-
export const integer = option<number>({
58+
export const integer = build({
5259
parse: input => {
5360
if (!/^[0-9]+$/.test(input)) throw new Error(`Expected an integer but received: ${input}`)
5461
return parseInt(input, 10)
5562
},
5663
})
5764

58-
const stringFlag = option()
65+
export function option<T>(options: {parse: IOptionFlag<T>['parse']} & Partial<IOptionFlag<T>>) {
66+
return build<T>(options)()
67+
}
68+
69+
const stringFlag = build({})
5970
export {stringFlag as string}
6071

6172
export const defaultFlags = {
6273
color: boolean({allowNo: true}),
6374
}
6475

65-
export interface Input { [name: string]: IFlag<any> }
76+
export interface Output {[name: string]: any}
77+
export type Input<T extends Output> = { [P in keyof T]: IFlag<T[P]> }

src/index.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
1-
export {ParserOutput, OutputArgs, OutputFlags} from './parse'
21
import * as args from './args'
2+
import {OutputArgs, OutputFlags, ParserOutput} from './parse'
33
export {args}
44
import * as flags from './flags'
55
export {flags}
66
export {flagUsages} from './help'
77
import {deps} from './deps'
8-
import {ParserOutput} from './parse'
98

10-
export interface ParserInput {
11-
argv?: string[]
12-
flags?: flags.Input
9+
export interface ParserInput<TFlags extends flags.Output> {
10+
flags?: flags.Input<TFlags>
1311
args?: args.Input
12+
argv?: string[]
1413
strict?: boolean
1514
}
1615

17-
export function parse(options: ParserInput): ParserOutput {
16+
export function parse<TFlags, TArgs>(options: ParserInput<TFlags>): ParserOutput<TFlags, TArgs> {
1817
const input = {
19-
args: (options.args || []).map(a => deps.args.newArg(a)),
18+
args: (options.args || []).map((a: any) => deps.args.newArg(a as any)),
2019
argv: options.argv || process.argv.slice(2),
2120
flags: {
2221
color: deps.flags.defaultFlags.color,
23-
...((options.flags || {})),
22+
...((options.flags || {})) as any,
2423
},
2524
strict: options.strict !== false,
2625
}
2726
const parser = new deps.parse.Parser(input)
2827
const output = parser.parse()
2928
deps.validate.validate({input, output})
30-
return output
29+
return output as any
3130
}
31+
32+
export {OutputFlags, OutputArgs, ParserOutput}

src/parse.ts

+13-13
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ try {
1515
debug = () => {}
1616
}
1717

18-
export interface OutputArgs { [k: string]: any }
19-
export interface OutputFlags { [k: string]: any }
20-
export interface ParserOutput {
21-
flags: OutputFlags
22-
args: { [k: string]: any }
18+
export type OutputArgs<T extends ParserInput['args']> = { [P in keyof T]: any }
19+
export type OutputFlags<T extends ParserInput['flags']> = { [P in keyof T]: any }
20+
export interface ParserOutput<TFlags extends OutputFlags<any>, TArgs extends OutputArgs<any>> {
21+
flags: TFlags
22+
args: TArgs
2323
argv: string[]
2424
raw: ParsingToken[]
2525
}
@@ -30,16 +30,16 @@ export type ParsingToken = ArgToken | FlagToken
3030

3131
export interface ParserInput {
3232
argv: string[]
33-
flags: Flags.Input
33+
flags: Flags.Input<any>
3434
args: Arg<any>[]
3535
strict: boolean
3636
}
3737

38-
export class Parser {
38+
export class Parser<T extends ParserInput, TFlags extends OutputFlags<T['flags']>, TArgs extends OutputArgs<T['args']>> {
3939
private readonly argv: string[]
4040
private readonly raw: ParsingToken[] = []
41-
private readonly booleanFlags: { [k: string]: Flags.IBooleanFlag }
42-
constructor(readonly input: ParserInput) {
41+
private readonly booleanFlags: { [k: string]: Flags.IBooleanFlag<any> }
42+
constructor(readonly input: T) {
4343
this.argv = input.argv.slice(0)
4444
this._setNames()
4545
this.booleanFlags = _.pickBy(input.flags, f => f.type === 'boolean') as any
@@ -132,17 +132,17 @@ export class Parser {
132132
}
133133
}
134134

135-
private _args(argv: any[]): OutputArgs {
136-
const args: OutputArgs = {}
135+
private _args(argv: any[]): TArgs {
136+
const args = {} as any
137137
for (let i = 0; i < this.input.args.length; i++) {
138138
const arg = this.input.args[i]
139139
args[arg.name!] = argv[i]
140140
}
141141
return args
142142
}
143143

144-
private _flags(): OutputFlags {
145-
const flags: OutputFlags = {}
144+
private _flags(): TFlags {
145+
const flags = {} as any
146146
for (const token of this._flagTokens) {
147147
const flag = this.input.flags[token.flag]
148148
if (!flag) throw new Error(`Unexpected flag ${token.flag}`)

src/validate.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {deps} from './deps'
22
import {ParserInput, ParserOutput} from './parse'
33

4-
export function validate(parse: { input: ParserInput; output: ParserOutput }) {
4+
export function validate(parse: { input: ParserInput; output: ParserOutput<any, any> }) {
55
function validateArgs() {
66
const maxArgs = parse.input.args.length
77
if (parse.input.strict && parse.output.argv.length > maxArgs) {

test/parse.test.ts

+25-4
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,15 @@ See more help with --help`)
208208
describe('multiple flags', () => {
209209
it('parses multiple flags', () => {
210210
const out = parse({
211-
argv: ['--bar', 'a', '--bar=b', '--foo=c'],
211+
argv: ['--bar', 'a', '--bar=b', '--foo=c', '--baz=d'],
212212
flags: {
213-
bar: flags.string({multiple: true}),
214213
foo: flags.string(),
214+
bar: flags.string({multiple: true}),
215+
baz: flags.string({required: true}),
215216
},
216217
})
217-
expect(out.flags.foo.toUpperCase()).to.equal('C')
218+
expect(out.flags.foo!.toUpperCase()).to.equal('C')
219+
expect(out.flags.baz.toUpperCase()).to.equal('D')
218220
expect(out.flags.bar.join('|')).to.equal('a|b')
219221
})
220222
})
@@ -341,8 +343,27 @@ See more help with --help`)
341343
})
342344

343345
describe('custom option', () => {
346+
it('can pass parse fn', () => {
347+
const foo = flags.option({char: 'f', parse: () => 100})
348+
const out = parse({
349+
argv: ['-f', 'bar'],
350+
flags: {foo},
351+
})
352+
expect(out.flags).to.deep.include({foo: 100})
353+
})
354+
})
355+
356+
describe('build', () => {
357+
it('can pass parse fn', () => {
358+
const foo = flags.build({char: 'f', parse: () => 100})
359+
const out = parse({
360+
argv: ['-f', 'bar'],
361+
flags: {foo: foo()},
362+
})
363+
expect(out.flags).to.deep.include({foo: 100})
364+
})
344365
it('does not require parse fn', () => {
345-
const foo = flags.option({char: 'f'})
366+
const foo = flags.build({char: 'f'})
346367
const out = parse({
347368
argv: ['-f', 'bar'],
348369
flags: {foo: foo()},

0 commit comments

Comments
 (0)