ndruger/zod-opts(新しいタブで開きます)
主要言語: TypeScript
Command-line argument parsing and validation with Zod.
- star 数
- 22
- watcher 数
- 0
- fork 数
- 2
- issue 数
- 2
- 最終更新
- 2026/10/29
主要言語: TypeScript
Command-line argument parsing and validation with Zod.
A library that simplifies the process of parsing and validating command-line arguments using the Zod validation library
npm install zod-opts # npm yarn add zod-opts # yarn
zod-opts supports both Zod v3 (3.25.0+) and Zod v4.
There is a behavioral difference between Zod v3 and v4 when using .default().optional():
const schema = z.string().default("foo").optional(); // Zod v3: schema.parse(undefined) → undefined // Zod v4: schema.parse(undefined) → "foo"
| Pattern | Zod v3 | Zod v4 |
|---|---|---|
z.string().optional() |
undefined |
undefined |
z.string().default("foo") |
"foo" |
"foo" |
z.string().optional().default("foo") |
"foo" |
"foo" |
z.string().default("foo").optional() |
undefined |
"foo" |
In Zod v4, .default() always applies regardless of .optional(). If you're migrating from v3 to v4, review any usage of .default().optional() as the behavior has changed.
Recommendation: Use .default("foo") alone if you want a default value. The .optional() suffix is redundant in Zod v4.
entries, arrays with element), so more v4 schemas are accepted without code changes.optional/effects/pipe/nullable/readonly are resolved before building option metadata, matching Zod v4 behavior.ParseError messages early; validation no longer silently assumes definitions exist. This improves feedback for mis-typed flags when running under either Zod v3 or v4.File: simple.ts
import { z } from "zod"; import { parser } from "zod-opts"; const parsed = parser() .options({ option1: { type: z.boolean().default(false), alias: "a", }, option2: { type: z.string(), }, }) .parse(); // same with .parse(process.argv.slice(2)) // parsed is inferred as { option1: boolean, option2: string } console.log(parsed);
# Valid options $ node simple.js --option1 --option2=str # or `node simple.js -a --option2 str` { option1: true, option2: 'str' } # Help $ node simple.js --help Usage: simple.js [options] Options: -h, --help Show help -a, --option1 (default: false) --option2 <string> [required] # Invalid options show help and make exit(1) $ node simple.js Required option is missing: option2 Usage: simple.js [options] Options: -h, --help Show help -a, --option1 (default: false) --option2 <string> [required]
File: complex.ts
import { z } from "zod"; // import { parser } from "zod-opts"; import { parser } from "../src/index"; const parsed = parser() .name("scriptA") // script name on Usage .version("1.0.0") // version on Usage .options({ option1: { // if default() is specified, it will be optional option. type: z.string().describe("description of option").default("default"), argumentName: "NameA", // used in Usage. }, option2: { type: z .string() .regex(/[a-z]+/) // you can use zod's various methods. .optional(), // if optional() is specified, it will be optional option. }, option3: { type: z.number().min(5), // accepts only number and greater than 5. }, option4: { type: z.enum(["a", "b", "c"]).default("b"), // accepts only "a", "b", "c" and default is "b". }, }) .args([ { // And required arguments. name: "arg1", type: z.string(), }, ]) .parse(); // parsed is inferred as below. // const parsed: { // option1: string; // option2?: string | undefined; // option3: number; // option4: "a" | "b" | "c"; // arg1: string; // } console.log(parsed);
# Valid options $ node complex.js --option3=10 arg_str { option1: 'default', option2: undefined, option3: 10, option4: 'b', arg1: 'arg_str' } # Help $ node complex.js --help Usage: scriptA [options] <arg1> Arguments: arg1 [required] Options: -h, --help Show help -V, --version Show version --option1 <NameA> description of option (default: "default") --option2 <string> --option3 <number> [required] --option4 <string> (choices: "a", "b", "c") (default: "b") # Version $ node complex.js --version 1.0.0
File: boolean.ts
const parsed = parser() .options({ option1: { type: z.boolean(), // required option. type is boolean }, option2: { type: z.boolean().default(false), // optional option. type is boolean }, option3: { type: z.boolean().optional(), // optional option. type is boolean|undefined }, option4: { type: z.boolean().default(false).optional(), // optional option. type is boolean|undefined }, }) .parse(); // parsed is inferred as below: // const parsed: { // option1: boolean; // option2: boolean; // option3?: boolean; // option4?: boolean; // }
You can use '--no-' prefix to set false(ex. --no-option1).
const parsed = parser() .options({ option1: { type: z.boolean().default(true), }, }) .parse(); console.log(parsed);
$ node script.js --no-option1
{ option1: false }File: enum.ts
const parsed = parser() .options({ option1: { type: z.enum(["a", "b"]), // required option. type is "a"|"b" }, option2: { type: z.enum(["a", "b"]).default("b"), // optional option. type is "a"|"b" }, option3: { type: z.enum(["a", "b"]).optional(), // optional option. type is "a"|"b"|undefined }, option4: { type: z.enum(["a", "b"]).default("b").optional(), // optional option. type is "a"|"b"|undefined }, }) .args([ { name: "position1", type: z.enum(["a", "b"]), // required arg. type is "a"|"b" }, ]) .parse(); // parsed is inferred as below: // const parsed: { // option1: "a" | "b"; // option2: "a" | "b"; // option3?: "a" | "b"; // option4?: "a" | "b"; // position1: "a" | "b"; // }; console.log(parsed);
CAUTION: program --opt opt_arg1 opt_arg2 pos_arg will be treated as opt=['opt_arg1' 'opt_arg2' 'pos_arg'].
In this case, the user should use program --opt opt_arg1 opt_arg2 -- pos_arg.
File: array_option.ts
const parsed = parser() .options({ opt: { type: z.array(z.string()), // required option. type is string[] // type: z.array(z.string()).default([]), // optional arg. type is string[] and default is [] }, }) .parse(); // parsed is inferred as below: // const parsed: { // opt: string[]; // }; console.log(parsed);
# Valid options $ node array_option.js --opt str1 str2 { opt: [ 'str1', 'str2' ] } # Invalid options (empty array is not permitted. use `.default([])` instead). $ node array_option.js --opt Option 'opt' needs value: opt Usage: array_option.js [options] Options: -h, --help Show help --opt <string ...> [required]
File: array_argument.ts
const parsed = parser() .args([ { name: "pos", type: z.array(z.string()), // required arg. type is string[] // type: z.array(z.string()).default([]), // optional arg. type is string[] and default is [] }, ]) .parse(); // parsed is inferred as below: // const parsed: { // pos: string[]; // }; console.log(parsed);
# Valid options $ node array_argument.js str1 str2 { pos: [ 'str1', 'str2' ] } # Invalid options (empty array is not permitted. use `.default([])` instead). $ node array_argument.js Required argument is missing: pos Usage: array_argument.js [options] <pos ...> Arguments: pos [required] Options: -h, --help Show help
You can use Zod's .refine() method to validate each option(e.g. z.string().refine((v) => v === "foo" || v === "bar", {message: "option1 must be foo or bar"}).
If you want to check combinations of options, you can use .validation() method. .validation() registers the custom validation function. And the function is called after default validation.
File: custom_validation.ts
const parsed = parser() .options({ option1: { type: z.number(), }, option2: { type: z.number(), }, }) .validation((parsed) => { if (parsed.option1 === parsed.option2) { throw Error("option1 and option2 must be different"); // or return "option1 and option2 must be different" } return true; }) .parse(); console.log(parsed);
# Valid options $ node custom_validation.js --option1=10 --option2=11 { option1: 10, option2: 11 } # Invalid options $ node custom_validation.js --option1=10 --option2=10 option1 and option2 must be different Usage: custom_validation.js [options] Options: -h, --help Show help --option1 <number> [required] --option2 <number> [required]
By default, zod-opts prints the message and help, then exits with 1 on errors and 0 on --help / --version. To change the exit code, register a callback with .onExit(). The callback receives the result and returns the exit code (integer 0-255). If it returns nothing, the default exit code is used. .onExit() is also available on parsers with subcommands.
result.reason tells why the error happened:
| reason | cause |
|---|---|
parse |
invalid arguments (unknown option, missing value, ...) |
schema |
the value does not match the Zod schema |
validation |
.validation() failed |
File: exit_code.ts
const parsed = parser() .options({ option1: { type: z.number(), }, option2: { type: z.number(), }, }) .validation((parsed) => { if (parsed.option1 === parsed.option2) { return "option1 and option2 must be different"; } return true; }) .onExit((result) => { // result.type: "error" | "help" | "version" // result.reason: "parse" | "schema" | "validation" (only when type is "error") if (result.type === "error" && result.reason !== "validation") { return 2; // usage error } // Returning nothing uses the default exit code (error: 1, help/version: 0) }) .parse();
$ node exit_code.js --option1=10; echo $? Required option is missing: option2 ... 2 $ node exit_code.js --option1=10 --option2=10; echo $? option1 and option2 must be different ... 1
If the callback throws, zod-opts does not print anything or exit, and the error is thrown from parse().
Please refer array types.
File command.ts
import { z } from "zod"; import { parser } from "zod-opts"; const command1 = command("command1") .options({ option1: { type: z.boolean().default(false), }, }) .action((parsed) => { // parsed is inferred as { option1: boolean } console.log("command2", parsed); }); const command2 = command("command2") .options({ option1: { type: z.string(), }, }) .action((parsed) => { // parsed is inferred as { option1: string } console.log("command2", parsed); }); parser().subcommand(command1).subcommand(command2).parse();
# Valid options $ node command.js command1 --option1 command1 { option1: true } # Invalid options $ node command.js command2 a Too many positional arguments Usage: command.js command2 [options] Options: -h, --help Show help --option1 <string> [required] # Global help $ node command.js --help Usage: command.js [options] <command> Commands: command1 command2 Options: -h, --help Show help # Command help $ node command.js command1 --help Usage: command.js command1 [options] Options: -h, --help Show help --option1 (default: false)
You can .showHelp() to show help message. And .getHelp() returns the help message.
If the parser has called with .version() method, The user can show the version with --version or -V option.
$ node complex.js --version 1.0.0
If you want to reuse Zod object type, you can define the type and use it in .options() and .args().
File: map_zod_object.ts
import { z } from "zod"; import { parser } from "zod-opts"; const OptionsSchema = z.object({ opt1: z.string(), opt2: z.number().optional(), pos1: z.enum(["a", "b"]), }); type Options = z.infer<typeof OptionsSchema>; function parseOptions(): Options { return parser() .name("scriptA") .version("1.0.0") .description("desc") .options({ opt1: { type: OptionsSchema.shape.opt1 }, opt2: { type: OptionsSchema.shape.opt2 }, }) .args([ { name: "pos1", type: OptionsSchema.shape.pos1, }, ]) .parse(); } const options = parseOptions(); console.log(options);
z.array() type in options().asyncParse()