Skip to content

Commit

Permalink
🤖 Merge PR DefinitelyTyped#61512 feat(node): types for util.parseArgs…
Browse files Browse the repository at this point in the history
… by @bakkot

* add types for util.parseArgs

* make types worse but valid on ancient versions of typescript

* add tests for util.parseArgs

* add missing config option, whoops

* more tests, plus tweak type for strict: false

* remove Exact constraint

* consistent pluralization

* address rbuckton comments

* export ParseArgsConfig type

* fix typo

Co-authored-by: John Gee <[email protected]>

Co-authored-by: John Gee <[email protected]>
  • Loading branch information
2 people authored and shlyren committed Aug 17, 2022
1 parent e628e54 commit 62e6501
Show file tree
Hide file tree
Showing 3 changed files with 266 additions and 1 deletion.
2 changes: 1 addition & 1 deletion types/node/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Type definitions for non-npm package Node.js 18.6
// Type definitions for non-npm package Node.js 18.7
// Project: https://nodejs.org/
// Definitions by: Microsoft TypeScript <https://github.com/Microsoft>
// DefinitelyTyped <https://github.com/DefinitelyTyped>
Expand Down
78 changes: 78 additions & 0 deletions types/node/test/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,81 @@ access('file/that/does/not/exist', (err) => {
{
util.stripVTControlCharacters('\u001B[4mvalue\u001B[0m'); // $ExpectType string
}

{
// util.parseArgs: happy path
// tslint:disable-next-line:no-object-literal-type-assertion
const config = {
allowPositionals: true,
options: {
foo: { type: 'string' },
bar: { type: 'boolean', multiple: true },
},
} as const;

// $ExpectType { values: { foo: string | undefined; bar: boolean[] | undefined; }; positionals: string[]; }
util.parseArgs(config);
}

{
// util.parseArgs: positionals not enabled
// tslint:disable-next-line:no-object-literal-type-assertion
const config = {
options: {
foo: { type: 'string' },
bar: { type: 'boolean', multiple: true },
},
} as const;

// @ts-expect-error
util.parseArgs(config).positionals[0];
}

{
// util.parseArgs: tokens
// tslint:disable-next-line:no-object-literal-type-assertion
const config = {
tokens: true,
allowPositionals: true,
options: {
foo: { type: 'string' },
bar: { type: 'boolean' },
},
} as const;

// tslint:disable-next-line:max-line-length
// $ExpectType { kind: "positional"; index: number; value: string; } | { kind: "option-terminator"; index: number; } | { kind: "option"; index: number; name: "foo"; rawName: string; value: string; inlineValue: boolean; } | { kind: "option"; index: number; name: "bar"; rawName: string; value: undefined; inlineValue: undefined; }
util.parseArgs(config).tokens[0];
}

{
// util.parseArgs: strict: false

// $ExpectType { values: { [longOption: string]: string | boolean | undefined; }; positionals: string[]; }
const result = util.parseArgs({
strict: false,
});
}

{
// util.parseArgs: strict: false

const result = util.parseArgs({
strict: false,
options: {
x: { type: 'string', multiple: true },
},
});
// $ExpectType (string | boolean)[] | undefined
result.values.x;
// $ExpectType string | boolean | undefined
result.values.y;
}

{
// util.parseArgs: config not inferred precisely
const config = {};

// $ExpectType { values: { [longOption: string]: string | boolean | (string | boolean)[] | undefined; }; positionals: string[]; tokens?: Token[] | undefined; }
const result = util.parseArgs(config);
}
187 changes: 187 additions & 0 deletions types/node/util.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,193 @@ declare module 'util' {
*/
encodeInto(src: string, dest: Uint8Array): EncodeIntoResult;
}

/**
* Provides a high-level API for command-line argument parsing. Takes a
* specification for the expected arguments and returns a structured object
* with the parsed values and positionals.
*
* `config` provides arguments for parsing and configures the parser. It
* supports the following properties:
*
* - `args` The array of argument strings. **Default:** `process.argv` with
* `execPath` and `filename` removed.
* - `options` Arguments known to the parser. Keys of `options` are the long
* names of options and values are objects accepting the following properties:
*
* - `type` Type of argument, which must be either `boolean` (for options
* which do not take values) or `string` (for options which do).
* - `multiple` Whether this option can be provided multiple
* times. If `true`, all values will be collected in an array. If
* `false`, values for the option are last-wins. **Default:** `false`.
* - `short` A single character alias for the option.
*
* - `strict`: Whether an error should be thrown when unknown arguments
* are encountered, or when arguments are passed that do not match the
* `type` configured in `options`. **Default:** `true`.
* - `allowPositionals`: Whether this command accepts positional arguments.
* **Default:** `false` if `strict` is `true`, otherwise `true`.
* - `tokens`: Whether tokens {boolean} Return the parsed tokens. This is useful
* for extending the built-in behavior, from adding additional checks through
* to reprocessing the tokens in different ways.
* **Default:** `false`.
*
* @returns The parsed command line arguments:
*
* - `values` A mapping of parsed option names with their string
* or boolean values.
* - `positionals` Positional arguments.
* - `tokens` Detailed parse information (only if `tokens` was specified).
*
*/
export function parseArgs<T extends ParseArgsConfig>(config: T): ParsedResults<T>;

interface ParseArgsOptionConfig {
type: 'string' | 'boolean';
short?: string;
multiple?: boolean;
}

interface ParseArgsOptionsConfig {
[longOption: string]: ParseArgsOptionConfig;
}

export interface ParseArgsConfig {
strict?: boolean;
allowPositionals?: boolean;
tokens?: boolean;
options?: ParseArgsOptionsConfig;
args?: string[];
}

/*
IfDefaultsTrue and IfDefaultsFalse are helpers to handle default values for missing boolean properties.
TypeScript does not have exact types for objects: https://github.com/microsoft/TypeScript/issues/12936
This means it is impossible to distinguish between "field X is definitely not present" and "field X may or may not be present".
But we expect users to generally provide their config inline or `as const`, which means TS will always know whether a given field is present.
So this helper treats "not definitely present" (i.e., not `extends boolean`) as being "definitely not present", i.e. it should have its default value.
This is technically incorrect but is a much nicer UX for the common case.
The IfDefaultsTrue version is for things which default to true; the IfDefaultsFalse version is for things which default to false.
*/
type IfDefaultsTrue<T, IfTrue, IfFalse> = T extends true
? IfTrue
: T extends false
? IfFalse
: IfTrue;

// we put the `extends false` condition first here because `undefined` compares like `any` when `strictNullChecks: false`
type IfDefaultsFalse<T, IfTrue, IfFalse> = T extends false
? IfFalse
: T extends true
? IfTrue
: IfFalse;

type ExtractOptionValue<T extends ParseArgsConfig, O extends ParseArgsOptionConfig> = IfDefaultsTrue<
T['strict'],
O['type'] extends 'string' ? string : O['type'] extends 'boolean' ? boolean : string | boolean,
string | boolean
>;

type ParsedValues<T extends ParseArgsConfig> =
& IfDefaultsTrue<T['strict'], unknown, { [longOption: string]: undefined | string | boolean }>
& (T['options'] extends ParseArgsOptionsConfig
? {
-readonly [LongOption in keyof T['options']]: IfDefaultsFalse<
T['options'][LongOption]['multiple'],
undefined | Array<ExtractOptionValue<T, T['options'][LongOption]>>,
undefined | ExtractOptionValue<T, T['options'][LongOption]>
>;
}
: {});

type ParsedPositionals<T extends ParseArgsConfig> = IfDefaultsTrue<
T['strict'],
IfDefaultsFalse<T['allowPositionals'], string[], []>,
IfDefaultsTrue<T['allowPositionals'], string[], []>
>;

type PreciseTokenForOptions<
K extends string,
O extends ParseArgsOptionConfig,
> = O['type'] extends 'string'
? {
kind: 'option';
index: number;
name: K;
rawName: string;
value: string;
inlineValue: boolean;
}
: O['type'] extends 'boolean'
? {
kind: 'option';
index: number;
name: K;
rawName: string;
value: undefined;
inlineValue: undefined;
}
: OptionToken & { name: K };

type TokenForOptions<
T extends ParseArgsConfig,
K extends keyof T['options'] = keyof T['options'],
> = K extends unknown
? T['options'] extends ParseArgsOptionsConfig
? PreciseTokenForOptions<K & string, T['options'][K]>
: OptionToken
: never;

type ParsedOptionToken<T extends ParseArgsConfig> = IfDefaultsTrue<T['strict'], TokenForOptions<T>, OptionToken>;

type ParsedPositionalToken<T extends ParseArgsConfig> = IfDefaultsTrue<
T['strict'],
IfDefaultsFalse<T['allowPositionals'], { kind: 'positional'; index: number; value: string }, never>,
IfDefaultsTrue<T['allowPositionals'], { kind: 'positional'; index: number; value: string }, never>
>;

type ParsedTokens<T extends ParseArgsConfig> = Array<
ParsedOptionToken<T> | ParsedPositionalToken<T> | { kind: 'option-terminator'; index: number }
>;

type PreciseParsedResults<T extends ParseArgsConfig> = IfDefaultsFalse<
T['tokens'],
{
values: ParsedValues<T>;
positionals: ParsedPositionals<T>;
tokens: ParsedTokens<T>;
},
{
values: ParsedValues<T>;
positionals: ParsedPositionals<T>;
}
>;

type OptionToken =
| { kind: 'option'; index: number; name: string; rawName: string; value: string; inlineValue: boolean }
| {
kind: 'option';
index: number;
name: string;
rawName: string;
value: undefined;
inlineValue: undefined;
};

type Token =
| OptionToken
| { kind: 'positional'; index: number; value: string }
| { kind: 'option-terminator'; index: number };

// If ParseArgsConfig extends T, then the user passed config constructed elsewhere.
// So we can't rely on the `"not definitely present" implies "definitely not present"` assumption mentioned above.
type ParsedResults<T extends ParseArgsConfig> = ParseArgsConfig extends T
? {
values: { [longOption: string]: undefined | string | boolean | Array<string | boolean> };
positionals: string[];
tokens?: Token[];
}
: PreciseParsedResults<T>;
}
declare module 'util/types' {
export * from 'util/types';
Expand Down

0 comments on commit 62e6501

Please sign in to comment.