CLI tool
dahlia/optique avatar
dahlia/optique

optique turns a TypeScript CLI grammar into types, help text and shell completions

Type-safe combinatorial CLI parser for TypeScript

737 stars18 forksTypeScriptMIT

At a glance

What is it?
dahlia/optique is a parser combinator library for TypeScript that infers the shape of your parsed arguments from the grammar you write, and derives help text, shell completions and man pages from the same definition. It deliberately stops at parsing, so you still own how the command runs.
Who is it for?
optique is worth adopting for a TypeScript CLI where the argument grammar is complex enough that hand written parsing has become the weak part of the codebase, and where you want help text and completions to stop drifting from the options you actually support.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Optique parses and validates, and stops there on purpose

The most useful thing to know about this library is what it refuses to do. The documentation states that Optique is a parsing library focused on extracting and validating command line arguments, that it does not dictate your application's structure, handle command execution, or provide scaffolding, and that it simply transforms command line input into well typed data structures. That boundary is unusual in a CLI library and it is the main design decision. There is no command router, no lifecycle hook and no application template to accept or reject. You build the parser, hand it to a runner, and receive an object whose type is known at compile time. The inspiration is stated as Haskell's optparse-applicative and TypeScript's Zod, which is a useful pairing: the first contributes the parser combinator model, the second contributes the schema validation ergonomics.

Optional flags and exclusive branches end up in the type

The core claim is that types model real CLI rules. Optional flags, mutually exclusive branches and dependent options are reflected directly in the inferred types, so an impossible combination is a type error rather than a runtime surprise. The combinators that make this work are listed explicitly: object(), or(), merge(), optional(), multiple(), map(), conditional() and passThrough(). A small parser looks like this:

typescript
import { option, constant } from "@optique/core/primitives";
import { object, or, merge } from "@optique/core/constructs";
import { optional } from "@optique/core/modifiers";
import { string, integer } from "@optique/core/valueparser";
import { run, print } from "@optique/run";

A reusable group of options is an object literal, and an optional option is wrapped:

typescript
const commonOptions = object({
  verbose: option("-v", "--verbose"),
  config: optional(option("-c", "--config", string())),
});

Branching works the same way. Each branch fixes a constant value for a field, which is what makes the alternatives distinguishable after parsing:

typescript
const localDeploy = object({
  mode: constant("local" as const),
  path: option("--path", string()),
  port: option("--port", integer({ min: 1000 })),
});

The cloud branch carries a different set of required options:

typescript
const cloudDeploy = object({
  mode: constant("cloud" as const),
  provider: option("--provider", string()),
  region: option("--region", string()),
  apiKey: option("--api-key", string()),
});

Merging the shared options with the choice between the two branches gives the whole grammar:

typescript
const parser = merge(
  commonOptions,
  or(localDeploy, cloudDeploy)
);

const config = run(parser, { help: "both" });

After that call the parsed value is a union of two shapes, so a check on the mode field narrows the rest of the object:

typescript
if (config.mode === "local") {
  print(`Deploying to ${config.path} on port ${config.port}.`);
} else {
  print(`Deploying to ${config.provider} in ${config.region}.`);
}

The value parsers are part of the same design. Strings and numbers are built in, and there are parsers for URLs, locales, UUIDs, and networking types such as port(), ipv4(), hostname() and email(). Temporal types live in a separate package, @optique/temporal, and validation is layered on through Standard Schema, Zod and Valibot adapters.

One grammar produces help, five shell completions and man pages

The output story is where a single source of definition pays off. From the same parser the library derives help text with usage formatting, labeled sections and colored output, shell completion scripts for Bash, zsh, fish, PowerShell and Nushell, and Unix man pages through the @optique/man package. Errors are part of that output too, with did you mean suggestions for typos and context aware formatting. The practical effect is that an option added to the grammar appears in every surface at once, which removes the class of bug where the help text describes flags that no longer exist. There is also a complete CLI setup with a run() function covering help, version and completion support, so a minimal program is a parser plus a run call. The gap to be aware of is that the same grammar has to be usable outside the process: completion scripts and man pages are generated from a definition that lives in your code.

The library is a family of packages, and the workspace shows it

The feature list names a whole family of packages, and the repository root backs that up with a packages directory and a pnpm-workspace.yaml alongside deno.json, mise.toml and a pnpm lock file. The base package is @optique/core, split into entry points for primitives, constructs, modifiers and value parsers, which is why the quick example imports from four different subpaths of the same package. On top of it sit @optique/run for the CLI setup, @optique/man for man page generation, and @optique/discover for splitting a large command tree across files with parser driven help and completion. Then come the integration packages: @optique/config for config files, @optique/env for environment variables, @optique/keyring for the OS credential store, @optique/derived-defaults for defaults computed from a first pass, and @optique/inquirer and @optique/clack for interactive prompts. Adopting optique is therefore an adoption of a package family, and each one adds a dependency and a decision.

Config files, environment variables and credentials are separate concerns

The integration packages exist because a real CLI has more inputs than argv, and optique treats each as a distinct layer rather than folding everything into one precedence chain inside the core. Config file support lives in @optique/config and loads configuration with Standard Schema validation, so Zod, Valibot, ArkType and others can validate the file contents. Environment variable binding lives in @optique/env with type safe parsing and fallback behaviour. The OS credential store is @optique/keyring, which fills in missing password options with async fallback behaviour, so a secret never has to travel as a command line argument where it would land in shell history. Derived defaults come from @optique/derived-defaults and compute a default from the first pass parse result without lowering the priority of an explicit command line argument. That last detail is the kind of precedence rule that is easy to get wrong by hand and worth reading in the docs before you rely on it.

Async parsers and option dependencies extend the grammar

Two extensions go beyond simple option shapes. Async parser support gives a type safe distinction between sync and async modes for parsers that validate against external sources such as git refs or remote APIs, which is the case where a parser cannot be a pure function of the argument list. Inter-option dependencies cover options whose valid values depend on other options, with dynamic validation and context aware shell completion, so the completion script itself can reflect the dependency. Command discovery through @optique/discover goes the other way, splitting a larger command tree into separate files while keeping help and completion driven by the parser rather than by hand written text. The examples directory shows this pattern in practice, with examples/gitique/ and examples/patterns/ at the top level of the repository.

Runtime support, licensing and the documentation site

Cross runtime consistency is an explicit goal, and the same parser model is meant to work in Deno, Node.js and Bun, with the repository carrying deno.json and deno.lock next to the pnpm workspace files. The licence is MIT, and a LICENSE file sits at the repository root. The package is published on both JSR and npm under the @optique scope, and CI runs through GitHub Actions with coverage reporting configured through codecov.yml. There are no GitHub releases, so the changelog lives in the repository instead, as CHANGES.md together with a changes.d/ directory, which is the convention for accumulating unreleased entries before they are folded into the file. The repository is not archived and the last push was on 2026-10-19, so the work is current. The documentation at optique.dev is the real entry point: the README points new users to a tutorial and a cookbook, and the site is where the per package reference lives.

Editorial conclusion

optique is worth adopting for a TypeScript CLI where the argument grammar is complex enough that hand written parsing has become the weak part of the codebase, and where you want help text and completions to stop drifting from the options you actually support. It is the wrong choice if you want a framework, since the project states that it does not dictate application structure, handle command execution or provide scaffolding, and it is the wrong choice if you are not ready to learn a combinator vocabulary in which options, branches and dependencies are values. Verify first that the packages you need exist for your runtime, that the value parsers cover your types, and that your CI can run the type checker and tests that the repository already configures.

Frequently asked questions

Where do I get optique and how do I start a project with it?

The packages are published on JSR and npm under the @optique scope, with @optique/core as the base package and @optique/run providing the CLI setup. The README points newcomers to the tutorial and the cookbook on the documentation site at optique.dev.

Does optique run my command or structure my application?

No. The documentation states that Optique focuses on extracting and validating command line arguments, and that it does not dictate application structure, handle command execution or provide scaffolding. It transforms command line input into well typed data structures, and the rest of the program is yours.

How do I express mutually exclusive options in optique?

You build each branch as its own object with a constant value for the discriminating field, then combine them with the or() combinator from @optique/core/constructs. The resulting type is a union, so checking the constant field narrows which options exist.

Can optique generate shell completions and man pages?

Yes. Completion script generation is automatic for Bash, zsh, fish, PowerShell and Nushell, and Unix man pages are generated from the same parser definitions through the @optique/man package. The README describes this as one parser, many outputs, alongside derived help text.

Does optique read values from config files or environment variables?

Yes, through separate packages. @optique/config loads config files with Standard Schema validation, @optique/env binds options to environment variables with type safe parsing and fallback behaviour, and @optique/keyring fills missing password options from the OS credential store with async fallback.

Official sources

  1. dahlia/optique on GitHub
  2. Issues
  3. License: MIT
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dahlia-optique.svg)](https://hysenlabs.com/projects/dahlia-optique)

Was this useful?