Addition: opts.allowMultipleFlagValues & more accurate return flag types

opts.allowMultipleFlagValues -> returns flag values as array and add duplicate flags to the array instead of picking the last one (eg ["--foo", "hello", "--foo=world!"] -> { foo: ["hello", "world!"] }).
    Flag return types are now typed as Partials if a default is not present and opts.allowMultipleFlagValues is not enabled.
This commit is contained in:
Oxtaly 2025-06-15 05:18:54 +02:00
parent c16dbc7d56
commit 86b8cfcfaa
2 changed files with 62 additions and 5 deletions

View File

@ -210,6 +210,7 @@ function validateAndFillDefaults(opts) {
validateOrDefaultIfUnset(opts, 'allowSingularDashLongFlags', 'boolean', false); validateOrDefaultIfUnset(opts, 'allowSingularDashLongFlags', 'boolean', false);
validateOrDefaultIfUnset(opts, 'lowerCaseFlagValues', 'boolean', false); validateOrDefaultIfUnset(opts, 'lowerCaseFlagValues', 'boolean', false);
validateOrDefaultIfUnset(opts, 'lowerCaseInputValues', 'boolean', false); validateOrDefaultIfUnset(opts, 'lowerCaseInputValues', 'boolean', false);
validateOrDefaultIfUnset(opts, 'allowMultipleFlagValues', 'boolean', false);
if(opts.shouldStopParsingFunc !== null) if(opts.shouldStopParsingFunc !== null)
validateOrDefaultIfUnset(opts, 'shouldStopParsingFunc', 'function', null); validateOrDefaultIfUnset(opts, 'shouldStopParsingFunc', 'function', null);
@ -825,9 +826,10 @@ function parseShorthandFlags(shorthandFlags, shorthandString) {
// TODO: Fix typescript types of returned flags to include opts.resolveFlagValueTypes into it as well as checking if it has a default and if not make it an optional?, and type never if it's not in the flags object // TODO: Fix typescript types of returned flags to include opts.resolveFlagValueTypes into it as well as checking if it has a default and if not make it an optional?, and type never if it's not in the flags object
/** /**
* @template {FlagsI} Flags * @template {FlagsI} Flags
* @template {{ [K in keyof Flags]: Flags[K]['type'] extends 'boolean' ? boolean : Flags[K]['type'] extends 'number' ? number : Flags[K]['type'] extends 'string' ? string : string | boolean | number }} FlagsReturn * @template {ParserOpts<Flags>} Opts
* @template {import('./types.d.ts').FlagsReturn<Opts>} FlagsReturn
* @param {string[]} argv * @param {string[]} argv
* @param {ParserOpts<Flags>} opts * @param {Opts} opts
* @returns {{ input: string[], flags: FlagsReturn, unparsed: string[] } }} * @returns {{ input: string[], flags: FlagsReturn, unparsed: string[] } }}
*/ */
function parser(argv, opts) { function parser(argv, opts) {
@ -1060,6 +1062,13 @@ function parser(argv, opts) {
const flagName = flag.config?.name || flag.key; const flagName = flag.config?.name || flag.key;
if(opts.allowMultipleFlagValues) {
if(!Array.isArray(flagReturnObj[flagName]))
flagReturnObj[flagName] = [flag.value];
else
flagReturnObj[flagName].push(flag.value);
return
}
flagReturnObj[flagName] = flag.value; flagReturnObj[flagName] = flag.value;
}) })
@ -1067,10 +1076,16 @@ function parser(argv, opts) {
Object.entries(opts.flags).forEach(([flagName, flagConfig]) => { Object.entries(opts.flags).forEach(([flagName, flagConfig]) => {
if(typeof flagReturnObj[flagName] !== 'undefined') if(typeof flagReturnObj[flagName] !== 'undefined')
return; return;
if(opts.allowMultipleFlagValues) {
if(typeof flagConfig.default === 'undefined')
return flagReturnObj[flagName] = [];
return flagReturnObj[flagName] = [flagConfig.default];
}
/** Not a needed check as you can tell, but present to explicitly highlight that behavior in the code */ /** Not a needed check as you can tell, but present to explicitly highlight that behavior in the code */
if(typeof flagConfig.default === 'undefined') if(typeof flagConfig.default === 'undefined')
return flagReturnObj[flagName] = undefined; return flagReturnObj[flagName] = undefined;
flagReturnObj[flagName] = flagConfig.default; return flagReturnObj[flagName] = flagConfig.default;
}); });
/// @ts-ignore - falsly wrong flags type /// @ts-ignore - falsly wrong flags type

46
types.d.ts vendored
View File

@ -11,6 +11,8 @@ interface FlagT<T, Z> {
acceptsNaturalValue?: boolean acceptsNaturalValue?: boolean
}; };
export type FlagTypes = "string" | "boolean" | "bigint" | "number" | "any";
export type FlagAny = FlagT<string, "string"> | FlagT<boolean, "boolean"> | FlagT<number, "number"> | FlagT<bigint, "bigint"> | FlagT<string | number | bigint | boolean, "any"> export type FlagAny = FlagT<string, "string"> | FlagT<boolean, "boolean"> | FlagT<number, "number"> | FlagT<bigint, "bigint"> | FlagT<string | number | bigint | boolean, "any">
export interface FlagsI { [key: readonly string]: FlagAny } export interface FlagsI { [key: readonly string]: FlagAny }
@ -49,7 +51,7 @@ export interface ParserOpts<Flags extends FlagsI> {
/** Override to change how warnings are emitted @default console.warn */ /** Override to change how warnings are emitted @default console.warn */
warningLogger?(log: string): void; warningLogger?(log: string): void;
/** Default type for either unknown flags or flags without an explicit type being set @default "any" */ /** Default type for either unknown flags or flags without an explicit type being set @default "any" */
defaultFlagType?: "string" | "boolean" | "bigint" | "number" | "any"; defaultFlagType?: FlagTypes;
/** /**
* Behavior when input does not follow the provided flags constraints (eg: flag assigned value (--flag=value) not being the correct type). * Behavior when input does not follow the provided flags constraints (eg: flag assigned value (--flag=value) not being the correct type).
* *
@ -125,6 +127,24 @@ export interface ParserOpts<Flags extends FlagsI> {
* @default null * @default null
*/ */
parseFilterFunc?(arg: string, index: number, argv: string[], argMeta: argMeta, input: string[]): boolean parseFilterFunc?(arg: string, index: number, argv: string[], argMeta: argMeta, input: string[]): boolean
/**
* Allows multiple flag values & changes the returned flags to always be arrays, with the default being the first value if the flag was not set, or an
* empty array if the flag does not have a default but was not present.
* @example
* ```js
* const { flags } = parser(["--flag=hello", "--flag=world"], {
* flags: {
* defaultedFlag: { default: "missing", type: "string" },
* missingFlag: { type: "string" },
* },
* allowMultipleFlagValues: true,
* allowUnknownFlags: true,
* });
* flags === { flag: ["hello", "world"], defaultedFlag: ["missing"], missingFlag: [] }
* ```
* @default false
*/
allowMultipleFlagValues?: boolean
} }
export type argMeta = { export type argMeta = {
@ -146,4 +166,26 @@ export type argMeta = {
function __type__getType(e: any) { return typeof e }; function __type__getType(e: any) { return typeof e };
export type JSTypes = ReturnType<typeof __type__getType> export type JSTypes = ReturnType<typeof __type__getType>;
export type FlagReturnType<FlagType extends FlagTypes> =
FlagType extends 'boolean' ? boolean
: FlagType extends 'number' ? number
: FlagType extends 'bigint' ? bigint
: FlagType extends 'string' ? string
: string | boolean | number | bigint;
type ValueOf<T> = T[keyof T];
type OptionalKeys<T, K extends keyof T> = Pick<T, K> & Partial<Omit<T, K>>;
type _OptionalIfFlagDefaultNotPresent<T> = OptionalKeys<T, ValueOf<{ [V in keyof T]: 'default' extends keyof T[V] ? never : V }>>;
type _ResolveFlagTypes<Flags extends FlagsI, allowMultipleFlagValues extends boolean> = {
[K in keyof Flags]: allowMultipleFlagValues extends true
? FlagReturnType<Flags[K]['type']>[]
: FlagReturnType<Flags[K]['type']>
}
export type FlagsReturn<Opts extends ParserOpts> = _ResolveFlagTypes<
Opts['allowMultipleFlagValues'] extends true ? Opts['flags'] : _OptionalIfFlagDefaultNotPresent<Opts['flags']>,
Opts['allowMultipleFlagValues'] extends true ? true : false
>;