Object Arguments with Defaults and Validation

This example demonstrates how to work with object-valued options using dot-notation or JSON strings. Object options support:

  • Nested properties: Define complex configuration structures with type-safe nested objects
  • Default values: Provide defaults at any level (entire object or individual nested properties)
  • Required properties: Mark nested properties as required (only validated when parent object is present)
  • Validation: Validate the entire object or nested properties
  • Coercion: Transform the final object after parsing
  • Additional properties: Allow arbitrary properties with a specific type

Dot Notation Syntax

Object properties are accessed using dot notation on the command line:

  • --config.server.host localhost sets config.server.host to "localhost"
  • --config.server.port 8080 sets config.server.port to 8080
  • Multiple properties can be set independently

JSON String Input

Alternatively, you can pass the entire object as a JSON string:

  • --config '{"server": {"host": "example.com", "port": 8080}}'
  • JSON input can be mixed with dot notation
  • When mixed, the order matters (later values override earlier ones)

Default Value Behavior

  • If no config properties are provided, the entire default object is used
  • If some properties are provided, defaults are applied for missing nested properties
  • Default values work at any nesting level

Required Properties

Required properties within objects are conditionally required:

  • If the parent object is not present at all, nested required properties are not checked
  • If the parent object is present (any property is set), nested required properties are validated

For example, if database.host is marked as required:

  • --config.server.port 8080 is valid (database config not present)
  • --config.database.port 5432 is invalid (database config present but host is missing)

All Example Files

FILE EXPLORER
object-notation-cli.ts
1import cliForge from 'cli-forge';
2
3const cli = cliForge('object-arguments', {
4  builder: (args) =>
5    args.option('config', {
6      type: 'object',
7      description: 'Configuration object with nested properties',
8      properties: {
9        server: {
10          type: 'object',
11          description: 'Server configuration',
12          properties: {
13            host: {
14              type: 'string',
15              description: 'Server hostname',
16              default: 'localhost',
17            },
18            port: {
19              type: 'number',
20              description: 'Server port',
21              default: 3000,
22            },
23            ssl: {
24              type: 'boolean',
25              description: 'Enable SSL',
26              default: false,
27            },
28          },
29        },
30        database: {
31          type: 'object',
32          description: 'Database configuration',
33          properties: {
34            host: {
35              type: 'string',
36              description: 'Database hostname',
37              required: true, // This will only be required if database config is provided
38            },
39            port: {
40              type: 'number',
41              description: 'Database port',
42              default: 5432,
43            },
44            name: {
45              type: 'string',
46              description: 'Database name',
47              required: true, // This will only be required if database config is provided
48            },
49          },
50        },
51        features: {
52          type: 'array',
53          items: 'string',
54          description: 'Enabled features',
55          default: ['basic'],
56        },
57      },
58      // You can provide a default for the entire config object
59      default: {
60        server: {
61          host: 'localhost',
62          port: 3000,
63          ssl: false,
64        },
65        features: ['basic'],
66      },
67      // Validate the entire config object
68      validate: (config) => {
69        if (
70          config.server?.port &&
71          (config.server.port < 1 || config.server.port > 65535)
72        ) {
73          return 'Server port must be between 1 and 65535';
74        }
75        return true;
76      },
77      // Coerce can transform the final config object
78      coerce: (config) => {
79        // Add a computed property - use type assertion since we're extending the type
80        if (config.server) {
81          (config.server as { url?: string }).url = `${
82            config.server.ssl ? 'https' : 'http'
83          }://${config.server.host}:${config.server.port}`;
84        }
85        return config as typeof config & { server?: { url: string } };
86      },
87    }),
88  handler: (args) => {
89    console.log('Configuration:');
90    console.log(JSON.stringify(args.config, null, 2));
91
92    // Type-safe access to nested properties
93    if (args.config?.server) {
94      console.log(`\nServer will run at: ${args.config.server.url}`);
95    }
96
97    if (args.config?.database) {
98      console.log(
99        `Database: ${args.config.database.name} at ${args.config.database.host}:${args.config.database.port}`
100      );
101    }
102
103    if (args.config?.features) {
104      console.log(`Features: ${args.config.features.join(', ')}`);
105    }
106  },
107});
108
109// We export the CLI for a few reasons:
110// - Testing
111// - Composition (a CLI can be a subcommand of another CLI)
112// - Docs generation
113export default cli;
114
115// Calling `.forge()` executes the CLI. It's single parameter is the CLI args
116// and they default to `process.argv.slice(2)`.
117if (require.main === module) {
118  (async () => {
119    await cli.forge();
120  })();
121}
122