Skip to content

Construct combinators

Construct combinators are the high-level combinators that compose multiple parsers into complex, structured CLI interfaces. While primitive parsers handle individual options and arguments, construct combinators orchestrate them into cohesive applications with sophisticated behavior patterns like mutually exclusive commands, structured configuration, and modular option groups.

Understanding construct combinators is essential for building real-world CLI applications. They provide the architectural patterns you need to create maintainable, user-friendly interfaces that can grow in complexity without becoming unwieldy. Each construct combinator follows Optique's compositional philosophy: complex behavior emerges from combining simple, well-understood pieces.

The power of construct combinators lies in their ability to preserve full type safety while enabling complex composition. TypeScript automatically infers the result types of even deeply nested parser structures, ensuring that your parsed CLI data is fully typed without manual type annotations.

object() parser

The object() parser combines multiple named parsers into a single parser that produces a structured object. This is the primary way to group related options and arguments into logical units, creating the foundation for most CLI applications.

typescript
import { type 
InferValue
,
object
,
option
,
argument
} from "@optique/core/parser";
import {
string
,
integer
} from "@optique/core/valueparser";
const
serverConfig
=
object
({
name
:
argument
(
string
({
metavar
: "NAME" })),
port
:
option
("-p", "--port",
integer
({
min
: 1,
max
: 65535 })),
host
:
option
("-h", "--host",
string
()),
verbose
:
option
("-v", "--verbose")
}); type
ServerConfig
=
InferValue
<typeof
serverConfig
>;
// Type automatically inferred as above.

Labeled objects

You can provide a label for documentation and error reporting purposes. This label appears in help text to group related options:

typescript
const 
networkOptions
=
object
("Network Configuration", {
host
:
option
("--host",
string
()),
port
:
option
("--port",
integer
()),
ssl
:
option
("--ssl")
}); const
loggingOptions
=
object
("Logging Options", {
logLevel
:
option
("--log-level",
choice
(["debug", "info", "warn", "error"])),
logFile
:
optional
(
option
("--log-file",
string
()))
});

Parser priority

The object() parser uses the highest priority among its constituent parsers. This ensures that higher-priority parsers (like commands) within the object are tried before lower-priority parsers in other parts of the CLI structure.

tuple() parser

The tuple() parser combines multiple parsers into a sequential parser that produces a tuple (ordered heterogenous array) of results. Unlike object() which uses named fields, tuple() preserves the order and positional nature of its components.

typescript
import { type 
InferValue
,
tuple
,
option
,
argument
} from "@optique/core/parser";
import {
string
,
integer
} from "@optique/core/valueparser";
const
connectionTuple
=
tuple
([
option
("--host",
string
()),
option
("--port",
integer
()),
argument
(
string
({
metavar
: "DATABASE" }))
] as
const
);
type
Connection
=
InferValue
<typeof
connectionTuple
>;
// Type automatically inferred as above.

IMPORTANT

You need to use as const to ensure the tuple is treated as a fixed-length array for type inference. Without as const, TypeScript will infer it as a variable-length array, losing the tuple type.

Sequential parsing

The tuple() parser processes its component parsers in priority order (not array order), which means parsers with higher priority are tried first regardless of their position in the tuple:

typescript
// Even though argument is last in the array, it might be parsed first
// depending on the current parsing context and available input
const 
mixedTuple
=
tuple
([
option
("-v", "--verbose"), // Priority 10
command
("start",
argument
(
string
())), // Priority 15 - tried first
argument
(
string
()) // Priority 5
] as
const
);

Labeled tuples

Like object(), tuple() supports labels for documentation:

typescript
const 
coordinates
=
tuple
("Location", [
option
("--lat",
float
({
metavar
: "LATITUDE" })),
option
("--lon",
float
({
metavar
: "LONGITUDE" })),
optional
(
option
("--alt",
float
({
metavar
: "ALTITUDE" })))
] as
const
);
type
Coordinate
=
InferValue
<typeof
coordinates
>;
// Type automatically inferred as above.

Usage patterns

Tuples are useful when you need:

  • Ordered results where position matters
  • Integration with APIs that expect arrays
  • Processing of homogeneous but positionally significant data
typescript
const 
rangeParser
=
tuple
([
argument
(
integer
({
metavar
: "START" })),
argument
(
integer
({
metavar
: "END" }))
]); // Usage: myapp 10 20 // Result: [10, 20] const
config
=
parse
(
rangeParser
, ["10", "20"]);
if (
config
.
success
) {
const [
start
,
end
] =
config
.
value
;
console
.
log
(`Processing range ${
start
} to ${
end
}.`);
}

or() parser

The or() parser creates mutually exclusive alternatives, trying each alternative in order until one succeeds. This is fundamental for building CLIs with different modes of operation, subcommands, or mutually exclusive option sets.

typescript
import {
  type 
InferValue
,
command
,
constant
,
object
,
option
,
or
,
} from "@optique/core/parser"; import {
string
,
integer
} from "@optique/core/valueparser";
const
parser
=
or
(
// Server mode
object
({
mode
:
constant
("server"),
port
:
option
("-p", "--port",
integer
()),
host
:
option
("-h", "--host",
string
())
}), // Client mode
object
({
mode
:
constant
("client"),
connect
:
option
("-c", "--connect",
string
()),
timeout
:
option
("-t", "--timeout",
integer
())
}) ); type
Result
=
InferValue
<typeof
parser
>;
// Type automatically inferred as discriminated union.

Discriminated unions

The or() parser creates TypeScript discriminated unions when used with constant() parsers. This enables type-safe pattern matching:

typescript
const 
config
=
parse
(
parser
, ["-p", "8080", "-h", "localhost"]);
if (
config
.
success
) {
switch (
config
.
value
.
mode
) {
case "server": // TypeScript knows this is server config
console
.
log
(`Server running on ${
config
.
value
.
host
}:${
config
.
value
.
port
}.`);
break; case "client": // TypeScript knows this is client config
console
.
log
(`Connecting to ${
config
.
value
.
connect
} with timeout ${
config
.
value
.
timeout
}.`);
break; } }

Command alternatives

The most common use of or() is for subcommand dispatch:

typescript
const 
gitLike
=
or
(
command
("add",
object
({
type
:
constant
("add"),
files
:
multiple
(
argument
(
string
())),
all
:
optional
(
option
("-A", "--all"))
})),
command
("commit",
object
({
type
:
constant
("commit"),
message
:
option
("-m", "--message",
string
()),
amend
:
optional
(
option
("--amend"))
})),
command
("push",
object
({
type
:
constant
("push"),
remote
:
withDefault
(
option
("-r", "--remote",
string
()), "origin"),
force
:
optional
(
option
("-f", "--force"))
})) ); // Usage examples: // myapp add file1.txt file2.txt --all // myapp commit -m "Fix parser bug" --amend // myapp push --remote upstream --force

Alternative parsing strategies

You can also use or() for different parsing approaches of the same logical concept:

typescript
const 
flexibleConfig
=
or
(
// Configuration via individual options
object
({
source
:
constant
("options"),
host
:
option
("--host",
string
()),
port
:
option
("--port",
integer
()),
ssl
:
option
("--ssl")
}), // Configuration via config file
object
({
source
:
constant
("file"),
configFile
:
option
("-c", "--config",
string
())
}), // Configuration via URL
object
({
source
:
constant
("url"),
connectionString
:
option
("--url",
url
())
}) );

merge() parser

The merge() parser combines multiple object-generating parsers into a single unified parser, enabling modular CLI design through reusable option groups. While originally designed for object() parsers, it now accepts any parser that produces object-like values, including withDefault(), map(), and other transformative parsers. This is essential for building maintainable applications where related options can be shared across different commands or modes.

typescript
import { 
constant
,
merge
,
object
,
option
,
optional
,
or
} from "@optique/core/parser";
import {
string
,
integer
,
choice
} from "@optique/core/valueparser";
// Define reusable option groups const
networkOptions
=
object
("Network", {
host
:
option
("--host",
string
()),
port
:
option
("--port",
integer
({
min
: 1,
max
: 65535 }))
}); const
authOptions
=
object
("Authentication", {
username
:
option
("-u", "--user",
string
()),
password
:
optional
(
option
("-p", "--pass",
string
())),
token
:
optional
(
option
("-t", "--token",
string
()))
}); const
loggingOptions
=
object
("Logging", {
logLevel
:
option
("--log-level",
choice
(["debug", "info", "warn", "error"])),
logFile
:
optional
(
option
("--log-file",
string
()))
}); // Combine groups for different application modes const
devMode
=
merge
(
object
({
mode
:
constant
("development") }),
networkOptions
,
loggingOptions
); const
prodMode
=
merge
(
object
({
mode
:
constant
("production") }),
networkOptions
,
authOptions
,
loggingOptions
,
object
("Production", {
workers
:
option
("-w", "--workers",
integer
({
min
: 1 })),
configFile
:
option
("-c", "--config",
string
())
}) ); const
applicationConfig
=
or
(
devMode
,
prodMode
);

Advanced parser combinations

This feature is available since Optique 0.3.0.

The merge() parser can now combine various types of object-generating parsers, not just object() parsers. This enables sophisticated patterns like dependent options and conditional configurations:

typescript
import {
  type 
InferValue
,
flag
,
merge
,
object
,
option
,
withDefault
,
map
,
} from "@optique/core/parser"; import {
string
,
integer
} from "@optique/core/valueparser";
import {
run
} from "@optique/run";
// Dependent options pattern: options that are only available when a flag is set const
dependentOptions
=
withDefault
(
object
({
feature
:
flag
("-f", "--feature"),
config
:
option
("-c", "--config",
string
()),
level
:
option
("-l", "--level",
integer
())
}), {
feature
: false as
const
} as
const
); // Transform parser results const
transformedConfig
=
map
(
object
({
host
:
option
("--host",
string
()),
port
:
option
("--port",
integer
())
}), ({
host
,
port
}) => ({
endpoint
: `${
host
}:${
port
}` })
); // Combine different parser types const
advancedParser
=
merge
(
dependentOptions
, // withDefault() parser
transformedConfig
, // map() result
object
({ // traditional object() parser
verbose
:
option
("-v", "--verbose")
}) ); type
Result
=
InferValue
<typeof
advancedParser
>;
const
result
:
Result
=
run
(
advancedParser
);

Dependent options pattern

A common use case is creating options that are only relevant when certain conditions are met:

typescript
const 
serverMode
=
withDefault
(
object
({
server
:
flag
("-s", "--server"),
port
:
option
("-p", "--port",
integer
()),
host
:
option
("-h", "--host",
string
()),
workers
:
option
("-w", "--workers",
integer
())
}), {
server
: false as
const
} as
const
); const
globalOptions
=
object
({
verbose
:
option
("-v", "--verbose"),
config
:
option
("-c", "--config",
string
())
}); const
appConfig
=
merge
(
serverMode
,
globalOptions
);
// Usage examples: // myapp -v -c config.json → server mode disabled // myapp -s -p 8080 -h localhost -v → server mode with port and host // myapp -s -p 3000 -w 4 -c prod.json → full server configuration

Type inference and merging

The merge() parser intelligently combines the types of all merged parsers, regardless of their original parser type:

typescript
const 
basicOptions
=
object
({
verbose
:
option
("-v", "--verbose"),
quiet
:
option
("-q", "--quiet")
}); const
advancedOptions
=
object
({
timeout
:
option
("--timeout",
integer
()),
retries
:
option
("--retries",
integer
())
}); const
allOptions
=
merge
(
basicOptions
,
advancedOptions
);
type
Options
=
InferValue
<typeof
allOptions
>;
// Type automatically inferred as above.

When combining different parser types, the merge result maintains full type safety while accounting for the unique characteristics of each parser:

typescript
const 
conditionalFeatures
=
withDefault
(
object
({
experimental
:
flag
("--experimental"),
debugLevel
:
option
("--debug-level",
integer
())
}), {
experimental
: false as
const
} as
const
); const
transformedSettings
=
map
(
object
({
theme
:
option
("--theme",
string
()),
lang
:
option
("--language",
string
())
}), ({
theme
,
lang
}) => ({
locale
: `${
lang
}_${
theme
.
toUpperCase
()}`,
settings
: {
theme
,
lang
}
}) ); const
complexConfig
=
merge
(
conditionalFeatures
,
transformedSettings
,
object
({
version
:
option
("--version",
string
())
}) ); type
ComplexConfig
=
InferValue
<typeof
complexConfig
>;
// Type automatically inferred with conditional fields and transformations.

concat() parser

This API is available since Optique 0.2.0.

The concat() parser combines multiple tuple() parsers into a single unified parser, enabling modular tuple design through reusable tuple groups. This is the tuple equivalent of merge() for objects, providing compositional flexibility for sequential, positional argument structures.

typescript
import { 
concat
,
tuple
,
option
,
parse
} from "@optique/core/parser";
import {
string
,
integer
} from "@optique/core/valueparser";
// Define reusable tuple groups const
basicFlags
=
tuple
([
option
("-v", "--verbose"),
option
("-q", "--quiet"),
] as
const
);
const
serverConfig
=
tuple
([
option
("-p", "--port",
integer
({
min
: 1,
max
: 65535 })),
option
("-h", "--host",
string
()),
] as
const
);
const
logConfig
=
tuple
([
option
("--log-level",
string
()),
option
("--log-file",
string
()),
] as
const
);
// Combine tuples for different application modes const
webServer
=
concat
(
basicFlags
,
serverConfig
);
const
fullServer
=
concat
(
basicFlags
,
serverConfig
,
logConfig
);
const
result
=
parse
(
fullServer
, [
"-v", "-p", "8080", "-h", "localhost", "--log-level", "info", "--log-file", "app.log" ]); if (
result
.
success
) {
const [
verbose
,
quiet
,
port
,
host
,
logLevel
,
logFile
] =
result
.
value
;
console
.
log
(`Server: ${
host
}:${
port
}, verbose: ${
verbose
}.`);
console
.
log
(`Logging: ${
logLevel
} to ${
logFile
}.`);
}

Type concatenation

The concat() parser intelligently flattens the types of all concatenated tuple parsers into a single tuple type:

typescript
const 
userInfo
=
tuple
([
option
("-n", "--name",
string
()),
option
("-a", "--age",
integer
()),
] as
const
);
const
preferences
=
tuple
([
option
("-t", "--theme",
string
()),
option
("-v", "--verbose"),
] as
const
);
const
combined
=
concat
(
userInfo
,
preferences
);
type
CombinedType
=
InferValue
<typeof
combined
>;
// Type automatically inferred as above.

Usage patterns

Concatenation is useful when you need:

  • Modular tuple construction from reusable components
  • Sequential argument processing with clear grouping
  • Building complex CLIs from simpler tuple building blocks
  • Maintaining positional semantics across grouped options
typescript
// Build authentication tuple
const 
authTuple
=
tuple
([
option
("-u", "--user",
string
()),
option
("-p", "--pass",
string
()),
] as
const
);
// Build connection tuple const
connectionTuple
=
tuple
([
option
("--host",
string
()),
option
("--port",
integer
()),
option
("--ssl"),
] as
const
);
// Build command arguments tuple const
commandTuple
=
tuple
([
argument
(
string
()), // command name
argument
(
string
()), // target file
] as
const
);
// Combine all for a database client const
dbClient
=
concat
(
authTuple
,
connectionTuple
,
commandTuple
);
// Usage: myapp -u admin -p secret --host db.example.com --port 5432 --ssl backup users.sql const
config
=
parse
(
dbClient
, [
"-u", "admin", "-p", "secret", "--host", "db.example.com", "--port", "5432", "--ssl", "backup", "users.sql" ]); if (
config
.
success
) {
const [
user
,
pass
,
host
,
port
,
ssl
,
command
,
file
] =
config
.
value
;
console
.
log
(`Connecting as ${
user
} to ${
host
}:${
port
}.`);
console
.
log
(`Running ${
command
} on ${
file
}.`);
}

Relationship to merge()

While merge() combines object parsers by merging their properties, concat() combines tuple parsers by flattening their positional elements:

CombinatorInput parsersResult type
merge()object() parsersMerged object with all properties
concat()tuple() parsersFlattened tuple with all elements

Both provide compositional design patterns but serve different structural needs in CLI applications.

longestMatch() parser

This API is available since Optique 0.3.0.

The longestMatch() parser combines multiple mutually exclusive parsers by selecting the parser that consumes the most input tokens. Unlike or() which returns the first successful match, longestMatch() tries all parsers and selects the one with the longest match. This enables context-aware parsing where more specific patterns take precedence over general ones.

typescript
import {
  type 
InferValue
,
argument
,
constant
,
flag
,
longestMatch
,
object
,
} from "@optique/core/parser"; import {
string
} from "@optique/core/valueparser";
const
globalHelp
=
object
({
type
:
constant
("global"),
help
:
flag
("--help"),
}); const
contextualHelp
=
object
({
type
:
constant
("contextual"),
command
:
argument
(
string
({
metavar
: "COMMAND" })),
help
:
flag
("--help"),
}); const
parser
=
longestMatch
(
globalHelp
,
contextualHelp
);
type
Result
=
InferValue
<typeof
parser
>;
// Usage examples: // myapp --help → globalHelp (1 token: --help) // myapp list --help → contextualHelp (2 tokens: list --help)

Longest match selection

The key behavior of longestMatch() is selecting the parser that consumes the most tokens from the input:

typescript
const 
shortPattern
=
object
({
type
:
constant
("short"),
help
:
flag
("--help"),
}); const
longPattern
=
object
({
type
:
constant
("long"),
command
:
argument
(
string
()),
help
:
flag
("--help"),
}); const
parser
=
longestMatch
(
shortPattern
,
longPattern
);
// Short pattern matches: consumes 1 token (--help) const
shortResult
=
parse
(
parser
, ["--help"]);
if (
shortResult
.
success
) {
console
.
log
(
shortResult
.
value
.
type
); // "short"
} // Long pattern matches: consumes 2 tokens (list --help) const
longResult
=
parse
(
parser
, ["list", "--help"]);
if (
longResult
.
success
) {
console
.
log
(
longResult
.
value
.
type
); // "long"
}

Context-aware help systems

The most common use case for longestMatch() is implementing context-aware help systems where command --help shows help for that specific command:

typescript
// Define your application commands
const 
addCommand
=
command
(
"add",
object
({
action
:
constant
("add"),
key
:
argument
(
string
({
metavar
: "KEY" })),
value
:
argument
(
string
({
metavar
: "VALUE" })),
}), ); const
listCommand
=
command
(
"list",
object
({
action
:
constant
("list"),
pattern
:
optional
(
option
("-p", "--pattern",
string
({
metavar
: "PATTERN" })),
), }), ); // Normal command parsing const
normalParser
=
object
({
help
:
constant
(false),
result
:
or
(
addCommand
,
listCommand
),
}); // Context-aware help parsing const
contextualHelpParser
=
object
({
help
:
constant
(true),
commands
:
multiple
(
argument
(
string
({
metavar
: "COMMAND" }))),
flag
:
flag
("--help"),
}); // Combine with longestMatch for intelligent help selection const
parser
=
longestMatch
(
normalParser
,
contextualHelpParser
);
// Normal usage works as expected: // myapp add key1 value1 → normalParser (3 tokens) // myapp list -p "*.txt" → normalParser (3 tokens) // Context-aware help automatically activates: // myapp list --help → contextualHelpParser (2 tokens: ["list"]) // myapp add --help → contextualHelpParser (2 tokens: ["add"])

Type inference and unions

Like or(), longestMatch() creates discriminated union types when used with parsers that produce different shaped objects:

typescript
const 
stringMode
=
object
({
mode
:
constant
("string" as
const
),
value
:
option
("-s", "--string",
string
()),
}); const
numberMode
=
object
({
mode
:
constant
("number" as
const
),
value
:
option
("-n", "--number",
integer
()),
}); const
parser
=
longestMatch
(
stringMode
,
numberMode
);
type
Config
=
InferValue
<typeof
parser
>;
// Type-safe pattern matching const
result
=
parse
(
parser
, ["-s", "hello"]);
if (
result
.
success
) {
switch (
result
.
value
.
mode
) {
case "string": // TypeScript knows this is string config
console
.
log
(`String value: ${
result
.
value
.
value
}.`);
break; case "number": // TypeScript knows this is number config
console
.
log
(`Number value: ${
result
.
value
.
value
}.`);
break; } }

Usage patterns and best practices

Use longestMatch() when you need:

  • Context-aware behavior: Different parsing based on how much input is consumed
  • Precedence by specificity: More specific patterns should win over general ones
  • Greedy matching: Always prefer the parser that consumes the most tokens
  • Help system integration: Context-sensitive help that shows relevant information
typescript
// Example: Git-like CLI with context-aware help
const 
gitAdd
=
command
("add",
object
({
action
:
constant
("add"),
files
:
multiple
(
argument
(
string
())),
all
:
optional
(
option
("-A", "--all")),
})); const
gitCommit
=
command
("commit",
object
({
action
:
constant
("commit"),
message
:
option
("-m", "--message",
string
()),
})); const
normalCommands
=
object
({
help
:
constant
(false),
command
:
or
(
gitAdd
,
gitCommit
),
}); const
helpParser
=
object
({
help
:
constant
(true),
commands
:
multiple
(
argument
(
string
())),
helpFlag
:
flag
("--help"),
}); const
cli
=
longestMatch
(
normalCommands
,
helpParser
);
// Usage patterns: // git add file.txt --all → normalCommands (gitAdd) // git commit -m "message" → normalCommands (gitCommit) // git add --help → helpParser (commands: ["add"]) // git commit --help → helpParser (commands: ["commit"]) // git --help → helpParser (commands: [])

Relationship to other combinators

CombinatorSelection strategyUse case
or()First successful matchMutually exclusive alternatives
longestMatch()Most tokens consumedContext-aware and greedy parsing
merge()Combines all parsersComposing complementary parsers

The longestMatch() combinator bridges the gap between simple alternatives (or()) and complex composition (merge()) by providing intelligent selection based on input consumption.