Command Validation
While static types go a long way towards stable CLIs, you often need a tighter control on the parameters your users will feed to your command line. For this reason, Clipanion provides an automatic (and optional) integration with Typanion, a library providing both static and runtime input validations and coercions.
Validating Options​
The Option.String declarator accepts a validator option. You can use it with the Clipanion predicates to enforce a specific shape for your option:
tst from 'typanion';ÂclassPowerCommand extendsCommand {a =Option .String ({validator :t .isNumber ()});b =Option .String ({validator :t .isNumber ()});Âasyncexecute () {this.context .stdout .write (`${this.a ** this.b }\n`);}}
tst from 'typanion';ÂclassPowerCommand extendsCommand {a =Option .String ({validator :t .isNumber ()});b =Option .String ({validator :t .isNumber ()});Âasyncexecute () {this.context .stdout .write (`${this.a ** this.b }\n`);}}
As you can see by hovering them, TypeScript correctly inferred that both this.a and this.b are numbers (coercing them from their original strings), and passing anything else at runtime will now trigger validation errors. You can apply additional rules by using the Typanion predicates, for example here to validate that something is a valid port:
tst from 'typanion';ÂconstisPort =t .applyCascade (t .isNumber (), [t .isInteger (),t .isInInclusiveRange (1, 65535),]);ÂclassServeCommand extendsCommand {port =Option .String ({validator :isPort });Âasyncexecute () {this.context .stdout .write (`Listening on ${this.port }\n`);}}
tst from 'typanion';ÂconstisPort =t .applyCascade (t .isNumber (), [t .isInteger (),t .isInInclusiveRange (1, 65535),]);ÂclassServeCommand extendsCommand {port =Option .String ({validator :isPort });Âasyncexecute () {this.context .stdout .write (`Listening on ${this.port }\n`);}}
Validating Commands​
While option-level validation is typically enough, in some cases you need to also enforce constraints in your application about the final shape. For instance, imagine a command where --foo cannot be used if --bar is used. For this kind of requirements, you can leverage the static schema declaration:
tst from 'typanion';ÂclassMyCommand extendsCommand {foo =Option .Boolean (`--foo`, false);bar =Option .Boolean (`--bar`, false);Âstaticschema = [t .hasMutuallyExclusiveKeys ([`foo`, `bar`]),];Âasyncexecute () {// ...}}
tst from 'typanion';ÂclassMyCommand extendsCommand {foo =Option .Boolean (`--foo`, false);bar =Option .Boolean (`--bar`, false);Âstaticschema = [t .hasMutuallyExclusiveKeys ([`foo`, `bar`]),];Âasyncexecute () {// ...}}
This schema will be run before executing the command, and will ensure that if any of foo and bar is true, then the other necessarily isn't.
Note however that schema doesn't contribute to the type inference, so checking whether one value is set won't magically refine the type for the other values.