Optique: Type-safe combinatorial CLI parser for TypeScript
24 points by hongminhee
24 points by hongminhee
Oh my god I’ve wanted an actually good CLI parser with types for years, yargs just does not cut it - this looks really nice.
nvm it looks like Claude wrote half the code here….
And the way you’re meant to do booleans feels a little weird but separately this doesn’t work, it results in an error because Optique thinks the option is required
const options = O.object({ watch: O.option("--watch") });
const result = O.run(options, { args: process.argv.slice(2) });
gives
Usage: foo.mts [[--watch]]
Error: Expected an option or argument, but got end of input.
what’s funny is that adding a separate option does fix the issue and --watch
is correctly optional.
Thanks for trying out Optique and for the feedback! You’re right about the boolean flag issue—I was able to reproduce it and it’s definitely a bug. I’ve opened an issue to track it. Edit: This has been fixed in v0.1.1 which was just released!
Regarding Claude’s involvement: I used Claude Code primarily for test writing and documentation (English isn’t my first language, so I needed help with that), but the core parsing logic was written by hand.
Appreciate you taking the time to check it out despite the rough edges!
You might also find cmd-ts interesting (or the less-popular-but-slightly-improved fork cmd-ts-too).
I love writing tiny CLIs in TypeScript, so I’m glad to see new stuff in this space. I genuinely think TypeScript is one of the best scripting languages available right now: it’s concise, nearly everyone knows a bit of it, it has a great type system, and Deno and Bun have usable stdlibs and let you run single-file scripts that embed their dependencies in the same file (like uv run
with inline deps has done for python). My secret weapon for scripting is dax, which lets you run shell commands like this:
const result = await $`echo 1`.text();
console.log(result); // 1
While it’s early, Optique directly addresses a problem I’ve been having with CLI parsing in TypeScript, namely sharing common sets of arguments between commands in a typesafe way. I’ve been using Cliffy, which I like a lot (commander is also good) but I don’t know if there’s really a first class way to share set of arguments. You can make a function that takes a command and adds arguments to it, but I’m not sure the typing works out when you do that.
One possible downside with Optique is that it only handles the parsing part, so once your args are parsed, you still have to do some kind of pattern match on the result to actually call what you want to call. That’s fine, but it feels duplicative of the structure you’ve already set out in the parser definition. By contrast, the commander-style builders let you stick the function for executing a command right in alongside the arguments.
Here are some of my small TS CLIs in case anyone is curious what they look like:
llm-cli
, a tiny CLI LLM client with replies, history, post to github gist, etcaipr
, a PR review tool wrapping llm-cli
that takes a repo and PR number and spits out a reviewdq
, like jq
but you pass it JS code to eval (blog post about it). This one uses the built-in Deno parseArgs
because it’s simple (they really should build something like Cliffy into the std lib).jprc
, a tool that creates a PR and uses llm-cli
to generate a branch name (see Overengineering PR create with jj)This looks nice. 99,9% of my code is python and I dream about ArgumentParser.parse_args() returning something that type checkers understand. I am not the only one but it does not seem like an easy problem to solve unfortunately.
How can it be type-safe if the host language isnt?
No, Typescript is strongly typed.
Typescript’s static type system is unsound and is designed to be easy to escape to dynamic typing. So I don’t think it’s strongly typed in that respect.
Javascript’s typing is weird in that it has a lot of ad-hoc implicit coercions, which is arguably not strongly typed either. But it is type-safe in the sense that you shouldn’t be able to cause type confusion errors in the runtime system.
Then Java and C++ aren’t “strongly typed” either, and neither is Rust since it has an unsafe mode. I don’t care; it’s academic hairsplitting, as far as I’m concerned, if you can’t call a language type-safe or strongly typed unless there are zero ways to break type safety. Meanwhile I’m getting stuff done in TS.
I didn’t say Typescript is bad or useless. Microsoft deliberately chose to make Typescript unsound for usability reasons, and that was clearly the right choice because Javascript developers found Typescript much more comfortable than Facebook’s Flow static analyser (which was sound).
But more generally, “strongly typed” isn’t a useful phrase. It has at least five distinct meanings, and it’s usually unclear which meaning(s) are intended by the phrase. (I certainly didn’t understand what you meant because your answer seemed like a non sequitur.) And all of these meanings are matters of degree: if you insist on being absolutist it gets really hard to have a useful conversation.
Statically typed as opposed to dynamically typed? Many languages have a mixture of the two, such as run time polymorphism in OO languages, or gradual type systems for dynamic languages. In this respect Typescript is a hybrid.
Sound static type system? As well as the question of whether it is supposed to be sound or not, it can be more or less difficult to break the type system deliberately or by accident.
Type safe at run time? Most languages have facilities for deliberately bypassing type safety; it can be more or less difficult to break type safety in ways that the programmer or language designer did not intend. Javascript is very safe, treating type safety failures as security vulnerabilities.
Fewer weird implicit coercions? There isn’t a total order here: for instance, C has implicit bool/int coercions, Rust does not; Rust has implicit deref, C does not. There’s a huge range in how much coercions are a convenience or a source of bugs.
How fancy is the type system? To what degree can you model properties of your program as types? This would have been a useful answer to @xnacly, saying that it’s possible for a program to use an fancy but unsound type system in a sound manner to catch bugs at compile time, but that’s a property of the program not the language.
There are probably other meanings, e.g. requiring runtime representations to be abstract; heavy type annotation burden; tho I don’t think I can get to 10 meanings :-)
Thanks for the calm reply; I’m afraid I was in a bad mood when I wrote that and it came out a bit harsh.
You’re right that I was conflating different aspects of strongly-typed, by your definition(s). I guess my definition is your #1, #3 and #5. Typescript is weak on #2 and #3 but that is IMO balanced by JS’s strong type safety: if you mess up types at runtime the worst that happens is an exception, not a crash or heap corruption. (TS unfortunately gets an F on #4, but that’s JS’s fault, and ESLint will save you from the worst of it.)
That article makes the correct argument that it doesn’t matter much in practice. And as the other commenter implies, it’s always a matter of practice.