Skip to content

Commit

Permalink
Use const type parameters for narrower typing, especially for choices
Browse files Browse the repository at this point in the history
  • Loading branch information
shadowspawn committed Nov 2, 2024
1 parent 8649fe9 commit 2ed4ecd
Show file tree
Hide file tree
Showing 6 changed files with 45 additions and 75 deletions.
11 changes: 2 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ This package offers TypeScript typings for `commander` which infer strong types
- all the parameters of the action handler, including the options
- options returned by `.opts()`

This package requires TypeScript 5.0 or higher (for `const` type parameters to `.choices()`).

The runtime is supplied by commander. This package is all about the typings.

Usage
Expand Down Expand Up @@ -62,12 +64,3 @@ const program = new Command()
.option('-d, --debug'); // program type includes chained options and arguments
const options = program.opts(); // smart type
```

Use a "const assertion" on the choices to narrow the option type from `string`:

```typescript
const program = new Command()
.addOption(new Option('--drink-size <size>').choices(['small', 'medium', 'large'] as const))
.parse();
const drinkSize = program.opts().drinkSize; // "small" | "medium" | "large" | undefined
```
12 changes: 7 additions & 5 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ export class Argument<
/**
* Set the default value, and optionally supply the description to be displayed in the help.
*/
default<T>(
default<const T>(
value: T,
description?: string,
): Argument<Usage, T, CoerceT, ArgRequired, ChoicesT>;
Expand All @@ -402,7 +402,7 @@ export class Argument<
/**
* Only allow argument value to be one of choices.
*/
choices<T extends readonly string[]>(
choices<const T extends readonly string[]>(
values: T,
): Argument<Usage, DefaultT, undefined, ArgRequired, T[number]>; // setting CoerceT to undefined because choices overrides argParser

Expand Down Expand Up @@ -448,7 +448,7 @@ export class Option<
/**
* Set the default value, and optionally supply the description to be displayed in the help.
*/
default<T>(
default<const T>(
value: T,
description?: string,
): Option<Usage, PresetT, T, CoerceT, Mandatory, ChoicesT>;
Expand All @@ -463,7 +463,9 @@ export class Option<
* new Option('--donate [amount]').preset('20').argParser(parseFloat);
* ```
*/
preset<T>(arg: T): Option<Usage, T, DefaultT, CoerceT, Mandatory, ChoicesT>;
preset<const T>(
arg: T,
): Option<Usage, T, DefaultT, CoerceT, Mandatory, ChoicesT>;

/**
* Add option name(s) that conflict with this option.
Expand Down Expand Up @@ -519,7 +521,7 @@ export class Option<
/**
* Only allow option value to be one of choices.
*/
choices<T extends readonly string[]>(
choices<const T extends readonly string[]>(
values: T,
): Option<Usage, PresetT, DefaultT, undefined, Mandatory, T[number]>; // setting CoerceT to undefined becuase choices overrides argParser

Expand Down
55 changes: 21 additions & 34 deletions tests/arguments.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ program

// mixed types possible, but unusual
program.addArgument(new Argument('[foo]').default(3)).action((foo, options) => {
expectType<string | number>(foo);
expectType<string | 3>(foo);
expectAssignable<OptionValues>(options);
});

Expand Down Expand Up @@ -247,28 +247,28 @@ program.command('sub3 [bar]').action((bar, options) => {

// choices
program
.addArgument(new Argument('<foo>').choices(['A', 'B'] as const))
.addArgument(new Argument('<foo>').choices(['A', 'B']))
.action((foo, options) => {
expectType<'A' | 'B'>(foo);
expectAssignable<OptionValues>(options);
});

program
.addArgument(new Argument('[foo]').choices(['A', 'B'] as const))
.addArgument(new Argument('[foo]').choices(['A', 'B']))
.action((foo, options) => {
expectType<'A' | 'B' | undefined>(foo);
expectAssignable<OptionValues>(options);
});

program
.addArgument(new Argument('<foo...>').choices(['A', 'B'] as const))
.addArgument(new Argument('<foo...>').choices(['A', 'B']))
.action((foo, options) => {
expectType<('A' | 'B')[]>(foo);
expectAssignable<OptionValues>(options);
});

program
.addArgument(new Argument('[foo...]').choices(['A', 'B'] as const))
.addArgument(new Argument('[foo...]').choices(['A', 'B']))
.action((foo, options) => {
expectType<('A' | 'B')[]>(foo);
expectAssignable<OptionValues>(options);
Expand All @@ -286,65 +286,52 @@ program

// default type ignored when arg is required
expectType<'C'>(
program
.addArgument(
new Argument('<foo>').default('D' as const).choices(['C'] as const),
)
.parse().processedArgs[0],
program.addArgument(new Argument('<foo>').default('D').choices(['C'])).parse()
.processedArgs[0],
);

// default before choices results in union when arg optional
expectType<'C' | 'D'>(
program
.addArgument(
new Argument('[foo]').default('D' as const).choices(['C'] as const),
)
.parse().processedArgs[0],
program.addArgument(new Argument('[foo]').default('D').choices(['C'])).parse()
.processedArgs[0],
);

// default after choices is still union type
expectType<'C' | 'D'>(
program
.addArgument(
new Argument('[foo]').choices(['C'] as const).default('D' as const),
)
.parse().processedArgs[0],
program.addArgument(new Argument('[foo]').choices(['C']).default('D')).parse()
.processedArgs[0],
);

// argRequired after choices still narrows type
expectType<'C'>(
program
.addArgument(new Argument('foo').choices(['C'] as const).argRequired())
.parse().processedArgs[0],
program.addArgument(new Argument('foo').choices(['C']).argRequired()).parse()
.processedArgs[0],
);

// argRequired before choices still narrows type
expectType<'C'>(
program
.addArgument(new Argument('foo').argRequired().choices(['C'] as const))
.parse().processedArgs[0],
program.addArgument(new Argument('foo').argRequired().choices(['C'])).parse()
.processedArgs[0],
);

// argOptional after choices narrows type and includes undefined
expectType<'C' | undefined>(
program
.addArgument(new Argument('foo').choices(['C'] as const).argOptional())
.parse().processedArgs[0],
program.addArgument(new Argument('foo').choices(['C']).argOptional()).parse()
.processedArgs[0],
);

// argOptional before choices narrows type and includes undefined
expectType<'C' | undefined>(
program
.addArgument(new Argument('foo').argOptional().choices(['C'] as const))
.parse().processedArgs[0],
program.addArgument(new Argument('foo').argOptional().choices(['C'])).parse()
.processedArgs[0],
);

// argParser after choices overrides choice type
expectType<number>(
program
.addArgument(
new Argument('<foo>')
.choices(['C'] as const)
.choices(['C'])
.argParser((val: string, prev: number) => prev + Number.parseInt(val)),
)
.parse().processedArgs[0],
Expand All @@ -356,7 +343,7 @@ expectType<'C'>(
.addArgument(
new Argument('<foo>')
.argParser((val: string, prev: number) => prev + Number.parseInt(val))
.choices(['C'] as const),
.choices(['C']),
)
.parse().processedArgs[0],
);
Expand Down
4 changes: 2 additions & 2 deletions tests/create-argument.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import { Command, createArgument } from '..';
{
const program = new Command();
program
.addArgument(createArgument('<value>').choices(['A', 'B', 'C'] as const))
.addArgument(createArgument('<value>').choices(['A', 'B', 'C']))
.action((arg) => {
expectType<'A' | 'B' | 'C'>(arg);
});
Expand All @@ -65,7 +65,7 @@ import { Command, createArgument } from '..';
{
const program = new Command();
program
.addArgument(createArgument('<value...>').choices(['A', 'B', 'C'] as const))
.addArgument(createArgument('<value...>').choices(['A', 'B', 'C']))
.action((arg) => {
expectType<('A' | 'B' | 'C')[]>(arg);
});
Expand Down
8 changes: 2 additions & 6 deletions tests/create-option.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,7 @@ import { Command, createOption } from '..';
const program = new Command();
const foo = program
.addOption(
createOption('-f, --foo <value>', 'description').choices([
'A',
'B',
'C',
] as const),
createOption('-f, --foo <value>', 'description').choices(['A', 'B', 'C']),
)
.opts().foo;
expectType<'A' | 'B' | 'C' | undefined>(foo);
Expand All @@ -78,7 +74,7 @@ import { Command, createOption } from '..';
const foo = program
.addOption(
createOption('-f, --foo <value...>', 'description')
.choices(['A', 'B', 'C'] as const)
.choices(['A', 'B', 'C'])
.makeOptionMandatory(),
)
.opts().foo;
Expand Down
30 changes: 11 additions & 19 deletions tests/options.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const o20 = program
.addOption(new Option('-c, --colour').default(0).preset(BigInt(3)))
.addOption(new Option('-C, --no-colour').preset('on'))
.opts();
expectType<{ colour: string | number | bigint }>(o20);
expectType<{ colour: 'on' | 0 | bigint }>(o20);

// multiple

Expand Down Expand Up @@ -194,43 +194,41 @@ expectType<{ debug?: true }>(us7);

// narrows required value to given choices
const co1 = program
.addOption(new Option('-d, --debug <val>').choices(['A', 'B'] as const))
.addOption(new Option('-d, --debug <val>').choices(['A', 'B']))
.opts();
expectType<{ debug?: 'A' | 'B' }>(co1);

// narrows optional value to union of given choices and true
const co2 = program
.addOption(new Option('-d, --debug [val]').choices(['A', 'B'] as const))
.addOption(new Option('-d, --debug [val]').choices(['A', 'B']))
.opts();
expectType<{ debug?: 'A' | 'B' | true }>(co2);

// narrows required option to given choices
const co3 = program
.addOption(
new Option('-d, --debug <val>')
.choices(['A', 'B'] as const)
.makeOptionMandatory(),
new Option('-d, --debug <val>').choices(['A', 'B']).makeOptionMandatory(),
)
.opts();
expectType<{ debug: 'A' | 'B' }>(co3);

// narrows variadic value to choices array
const co4 = program
.addOption(new Option('-d, --debug <val...>').choices(['A', 'B'] as const))
.addOption(new Option('-d, --debug <val...>').choices(['A', 'B']))
.opts();
expectType<{ debug?: ('A' | 'B')[] }>(co4);

// narrows optional variadic value to choices | true array
const co5 = program
.addOption(new Option('-d, --debug [val...]').choices(['A', 'B'] as const))
.addOption(new Option('-d, --debug [val...]').choices(['A', 'B']))
.opts();
expectType<{ debug?: ('A' | 'B')[] | true }>(co5);

// narrows required option with optional variadic value
const co6 = program
.addOption(
new Option('-d, --debug [val...]')
.choices(['A', 'B'] as const)
.choices(['A', 'B'])
.makeOptionMandatory(),
)
.opts();
Expand All @@ -240,32 +238,26 @@ expectType<{ debug: ('A' | 'B')[] | true }>(co6);
const co7 = program
.addOption(
new Option('-d, --debug <val...>')
.choices(['A', 'B'] as const)
.choices(['A', 'B'])
.makeOptionMandatory(),
)
.opts();
expectType<{ debug: ('A' | 'B')[] }>(co7);

// default before choices creates union type
const co8 = program
.addOption(
new Option('--foo <val>').default('D' as const).choices(['C'] as const),
)
.addOption(new Option('--foo <val>').default('D').choices(['C']))
.opts();
expectType<{ foo: 'C' | 'D' }>(co8);

// default after choices creates union type
const co9 = program
.addOption(
new Option('--foo <val>').choices(['C'] as const).default('D' as const),
)
.addOption(new Option('--foo <val>').choices(['C']).default('D'))
.opts();
expectType<{ foo: 'C' | 'D' }>(co9);

// make mandatory before choices makes option mandatory
const c10 = program
.addOption(
new Option('--foo <val>').makeOptionMandatory().choices(['C'] as const),
)
.addOption(new Option('--foo <val>').makeOptionMandatory().choices(['C']))
.opts();
expectType<{ foo: 'C' }>(c10);

0 comments on commit 2ed4ecd

Please sign in to comment.