OneOf Option

The oneOf option type lets a single flag accept values of different types. This is common in CLI tools where a flag doubles as a boolean toggle and a typed selector — for example --color / --no-color / --color=always.

When boolean is one of the value types, bare flags (--flag), negation (--no-flag), and the literals true / false are claimed exclusively by the boolean parser. All other values are tried against the remaining types in array order.

Boolean-or-string: the --color pattern

The most common use case for oneOf is a flag that toggles a feature on/off but also accepts specific modes. Think git --color, ls --color, or webpack --devtool.

1import cliForge from 'cli-forge';
2
3const cli = cliForge('color-demo')
4  .option('color', {
5    type: 'oneOf',
6    valueTypes: [
7      { type: 'string', choices: ['auto', 'always', 'never'] as const },
8      { type: 'boolean' },
9    ],
10    default: 'auto',
11    description: 'When to use colors in output',
12  })
13  .handler((args) => {
14    console.log(`Color mode: ${args.color}`);
15  });
16
17export default cli;
18
19if (require.main === module) {
20  (async () => {
21    await cli.forge();
22  })();
23}

The valueTypes array lists the types the flag can accept. Boolean is always tried last, regardless of where it appears in the array, but it has exclusive claim on three patterns:

InputResultWhy
--colortrueBare flag — only boolean can match with no value
--no-colorfalseNegation prefix — boolean-only
--color truetrueLiteral true/false — boolean claims these
--color always"always"Any other value — string parser wins

When the string entry includes choices, only those values are accepted. Passing --color invalid would fail validation because "invalid" is not in ['auto', 'always', 'never'].

Number-or-string: priority ordering

For non-boolean types, array order determines priority. The first parser that successfully handles the value wins. This is useful when a value could be interpreted as multiple types.

1import cliForge from 'cli-forge';
2
3const cli = cliForge('verbosity-demo')
4  .option('verbose', {
5    type: 'oneOf',
6    valueTypes: [{ type: 'number' }, { type: 'string' }],
7    description: 'Set verbosity as a number (0-5) or named level',
8  })
9  .handler((args) => {
10    if (args.verbose === undefined) {
11      console.log('Verbosity: default');
12    } else {
13      console.log(`Verbosity (${typeof args.verbose}): ${args.verbose}`);
14    }
15  });
16
17export default cli;
18
19if (require.main === module) {
20  (async () => {
21    await cli.forge();
22  })();
23}

Here number is listed before string, so --verbose 3 parses as the number 3, not the string "3". If the value is not numeric (like --verbose debug), the number parser fails and the string parser takes over.

Without boolean in valueTypes, bare flags (--verbose) and negation (--no-verbose) are errors — the option always requires an explicit value.

Inside object properties

oneOf can be used as the type for an object property. This lets a single dot-notation path accept values of different types. In this example, each filter property accepts either a shorthand string (--filter.prs ">5") or structured sub-properties via dot-notation (--filter.prs.min 1).

1import cliForge from 'cli-forge';
2
3const cli = cliForge('filter-demo')
4  .option('filter', {
5    type: 'object',
6    description: 'Filter criteria for search results',
7    properties: {
8      prs: {
9        type: 'oneOf',
10        description:
11          'Filter by PR count — a shorthand like ">5" or structured min/max',
12        valueTypes: [
13          {
14            type: 'object',
15            properties: {
16              min: { type: 'number' },
17              max: { type: 'number' },
18            },
19          },
20          { type: 'string' },
21        ],
22      } as any,
23      stars: {
24        type: 'oneOf',
25        description:
26          'Filter by star count — a shorthand like ">=100" or structured min/max',
27        valueTypes: [
28          {
29            type: 'object',
30            properties: {
31              min: { type: 'number' },
32              max: { type: 'number' },
33            },
34          },
35          { type: 'string' },
36        ],
37      } as any,
38    },
39  })
40  .handler((args) => {
41    const parts: string[] = [];
42    if (args.filter?.prs !== undefined) {
43      parts.push(`prs=${JSON.stringify(args.filter.prs)}`);
44    }
45    if (args.filter?.stars !== undefined) {
46      parts.push(`stars=${JSON.stringify(args.filter.stars)}`);
47    }
48    if (parts.length === 0) {
49      console.log('No filters applied');
50    } else {
51      console.log(`Filters: ${parts.join(', ')}`);
52    }
53  });
54
55export default cli;
56
57if (require.main === module) {
58  (async () => {
59    await cli.forge();
60  })();
61}

When the value is set directly (e.g. --filter.prs ">5"), the oneOf parser tries each value type in order. The object parser fails on ">5" (not valid JSON), so the string parser takes over.

When dot-notation is used (e.g. --filter.prs.min 1), the object parser recognizes the oneOf type and searches its valueTypes for an object branch that has the requested sub-property.

InputResult
--filter.prs ">5"{ prs: ">5" } — string branch wins
--filter.prs.min 1{ prs: { min: 1 } } — object branch, dot-notation
--filter.prs.min 1 --filter.prs.max 10{ prs: { min: 1, max: 10 } }
--filter.prs ">5" --filter.stars.min 100{ prs: ">5", stars: { min: 100 } } — mixed

All Example Files

FILE EXPLORER
color-output.ts
1import cliForge from 'cli-forge';
2
3const cli = cliForge('color-demo')
4  .option('color', {
5    type: 'oneOf',
6    valueTypes: [
7      { type: 'string', choices: ['auto', 'always', 'never'] as const },
8      { type: 'boolean' },
9    ],
10    default: 'auto',
11    description: 'When to use colors in output',
12  })
13  .handler((args) => {
14    console.log(`Color mode: ${args.color}`);
15  });
16
17export default cli;
18
19if (require.main === module) {
20  (async () => {
21    await cli.forge();
22  })();
23}
24