Skip to content

Repository files navigation

ZodOpts

NPM Version MIT License CI codecov Minified size

A library that simplifies the process of parsing and validating command-line arguments using the Zod validation library

Installation

npm install zod-opts # npm

yarn add zod-opts # yarn

Zod Compatibility

zod-opts supports both Zod v3 (3.25.0+) and Zod v4.

Behavioral Differences

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.

Changes introduced for Zod v4 support

  • Zod schema inspection now uses the compatibility layer to recognize v4-specific shapes (e.g., enums defined via entries, arrays with element), so more v4 schemas are accepted without code changes.
  • Optional/default handling is stricter: defaults wrapped in optional/effects/pipe/nullable/readonly are resolved before building option metadata, matching Zod v4 behavior.
  • Unknown commands/options/positionals now surface clear 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.

Quick Start

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

Options

Various option types

boolean types

  • .options() supports boolean type
  • .args() DOES NOT support boolean type

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;
// }
negatable 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 }

enum types

  • .options() supports enum type
  • .args() supports enum type

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);

array types

  • .options() supports array type
  • .args() supports array type
array option

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]
array positional arguments

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

Custom validation

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]

Variadic arguments

Please refer array types.

Commands

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)

Help

You can .showHelp() to show help message. And .getHelp() returns the help message.

Version

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

Advanced Usage

Reuse Zod object type

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);

Future work ideas

  • Support nested commands.
  • Support z.array() type in options().
  • Support custom callback to handle errors, help and exit().
  • asyncParse()

About

Command-line argument parsing and validation with Zod.

Topics

Resources

Stars

22 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages