Validation and Constraints
CLI Forge validates arguments after parsing, catching invalid input before your handler runs. You can restrict values with choices, enforce relationships between options, reject unknown flags, and write custom validation logic.
Limiting values with choices
Use the choices property to restrict an option to a set of allowed values. If the user provides a value outside this set, CLI Forge throws a validation error.
Pass a static array to narrow the TypeScript type to a union of allowed values:
.option('name', {
type: 'string',
description: 'The name to say hello to',
required: true,
// Choices limits valid values for the option.
// If the provided value is not in the choices array, an error will be thrown.
choices: ['sir', 'madame'],
})
Choices can also be a function that returns an array, which is useful for values computed at runtime:
.option('phrase', {
type: 'string',
default: 'hello',
// Choices can also be provided as a function that returns an array of valid values.
// This can be useful if the choices are dynamic or need to be calculated at runtime.
choices: () => ['hello', 'hi', 'hey'],
}),
When you use a static array, TypeScript narrows the argument type automatically (e.g., 'sir' | 'madame'). Choices are checked after coerce, so if you also use a coerce function, make sure it returns a value in the choices array.
Conflicts and implications
Use .conflicts() and .implies() to enforce relationships between options:
cli('conflicts-and-implications', {
builder: (args) =>
args
.option('source', {
describe: 'Source database',
type: 'string',
})
.option('target', {
describe: 'Target database',
type: 'string',
})
.option('dry-run', {
describe: 'Simulate the migration without making changes',
type: 'boolean',
})
.option('force', {
describe: 'Force the migration even if there are warnings',
type: 'boolean',
})
.option('backup', {
describe: 'Where should the backup be stored',
type: 'string',
})
// Conflicts creates mutually exclusive arguments. Validation will throw an error if both options are provided.
// In this case, it makes sense that the user wouldn't want to both simulate and force a migration.
.conflicts('dry-run', 'force')
// Implies creates mutually required arguments. Validation will throw an error if the first argument is provided without the second.
// Practically in this case, this means that if the user provides the --force option, they must also provide the --backup option.
.implies('force', 'backup'),
handler: (_args) => {
// ...
},
}).forge();
.conflicts('a', 'b')— optionsaandbcannot both be provided. Useful for mutually exclusive modes like--dry-runand--force..implies('a', 'b')— if optionais provided, optionbmust also be provided. Useful for options that only make sense together, like--forcerequiring--backup.
Both are checked during validation and produce clear error messages.
Strict mode
By default, CLI Forge collects unrecognized arguments into an unmatched array without raising errors. Strict mode changes this behavior to throw a validation error for any unrecognized argument:
const cli = cliForge('strict-mode-example')
// Enable strict mode - unmatched arguments will throw validation errors
.strict()
.option('name', {
type: 'string',
description: 'The name to greet',
default: 'World',
})
.command('$0', {
builder: (args) => args,
handler: (args) => {
console.log(`Hello, ${args.name}!`);
},
});
Non-strict mode (default)
Without strict mode, unknown arguments are silently collected. This is useful when your CLI wraps another tool and needs to pass arguments through:
const cli = cliForge('non-strict-mode-example')
// Strict mode is disabled by default
.strict(false)
.option('name', {
type: 'string',
description: 'The name to greet',
default: 'World',
})
.command('$0', {
builder: (args) => args,
handler: (args) => {
console.log(`Hello, ${args.name}!`);
console.log('Unmatched:', args.unmatched);
},
});
Custom validation
For validation logic that goes beyond choices and conflicts, use the validate property on an option:
cli('my-app')
.option('port', {
type: 'number',
validate: (value) => {
if (value < 1 || value > 65535) {
return 'Port must be between 1 and 65535';
}
return true;
},
});
The validate function receives the parsed value and should return true for valid input or an error message string for invalid input. Custom validators run after type coercion and choices checks.
Required options
Mark an option as required to ensure the user provides a value:
cli('my-app')
.option('name', {
type: 'string',
required: true,
});
If a required option is not provided via CLI arguments, environment variables, or configuration files, CLI Forge throws a validation error.
Validation order
CLI Forge runs validations in this order:
- Type coercion — convert raw strings to the declared type
- Defaults — apply default values for missing options
- Custom
coerce— run user-defined coerce functions - Required — check that required options have values
- Choices — check values against allowed sets
- Custom
validate— run user-defined validation functions - Conflicts — check mutual exclusivity
- Implications — check conditional requirements
- Strict unmatched check — reject unknown arguments (if strict mode is enabled)
Understanding this order matters when combining validators. For example, a coerce function runs before choices, so the coerced value must match a choice — not the raw input.
Quick reference
| Feature | Method/Property | Purpose |
|---|---|---|
| Allowed values | choices: [...] | Restrict to specific values |
| Mutual exclusion | .conflicts('a', 'b') | Prevent using both options |
| Conditional requirement | .implies('a', 'b') | Require b when a is set |
| Reject unknowns | .strict() | Error on unrecognized args |
| Custom logic | validate: (v) => ... | Arbitrary validation |
| Mandatory input | required: true | Ensure a value is provided |