Skip to content

Derived defaults

This API is available since Optique 1.2.0.

The @optique/derived-defaults package lets you compute fallback values from values that Optique has already parsed. This is useful when one option has a natural default that depends on another option, but the user should still be able to override it explicitly.

The fallback priority is:

  1. CLI argument
  2. Derived default
  3. Static default
  4. Error
deno add jsr:@optique/derived-defaults
npm add @optique/derived-defaults
pnpm add @optique/derived-defaults
yarn add @optique/derived-defaults
bun add @optique/derived-defaults

Basic usage

1. Create a derived-default context

Call createDerivedDefaults() with resolver functions. Each resolver receives the first-pass parse seed and returns either a fallback value or undefined when it has no value to provide.

The seed is best-effort: it is available only after Optique can produce a phase-two value, and some fields may still be missing while required options are unresolved. Write resolvers to check the fields they depend on and return undefined when the dependency is absent.

import { 
createDerivedDefaults
} from "@optique/derived-defaults";
const
derived
=
createDerivedDefaults
({
workspaceRoot
: (
parsed
: { readonly
serviceRoot
?: string }) =>
parsed
.
serviceRoot
== null
?
undefined
: `${
parsed
.
serviceRoot
}/workspace`,
});

2. Bind parsers to derived values

Use bindDerivedDefault() around the parser that should receive the derived fallback:

import { 
object
} from "@optique/core/constructs";
import {
option
} from "@optique/core/primitives";
import {
string
} from "@optique/core/valueparser";
import {
bindDerivedDefault
,
createDerivedDefaults
,
} from "@optique/derived-defaults"; const
derived
=
createDerivedDefaults
({
workspaceRoot
: (
parsed
: { readonly
serviceRoot
?: string }) =>
parsed
.
serviceRoot
== null
?
undefined
: `${
parsed
.
serviceRoot
}/workspace`,
}); const
parser
=
object
({
serviceRoot
:
option
("--service-root",
string
()),
workspaceRoot
:
bindDerivedDefault
(
option
("--workspace-root",
string
()), {
context
:
derived
.
context
,
key
: "workspaceRoot",
}), });

3. Run with contexts

Pass the derived-default context through contexts. Optique first parses the CLI input, then calls your resolvers with that parsed value and completes the bound parsers from the resulting annotations.

WARNING

bindDerivedDefault() only reads derived values when its context is registered with the runner. Omitting contexts: [derived.context] from the run() call causes the parser to fall back to a static default if one exists or fail as a missing value.

import { 
object
} from "@optique/core/constructs";
import {
option
} from "@optique/core/primitives";
import {
string
} from "@optique/core/valueparser";
import {
runAsync
} from "@optique/run";
import {
bindDerivedDefault
,
createDerivedDefaults
,
} from "@optique/derived-defaults"; const
derived
=
createDerivedDefaults
({
workspaceRoot
: (
parsed
: { readonly
serviceRoot
?: string }) =>
parsed
.
serviceRoot
== null
?
undefined
: `${
parsed
.
serviceRoot
}/workspace`,
}); const
parser
=
object
({
serviceRoot
:
option
("--service-root",
string
()),
workspaceRoot
:
bindDerivedDefault
(
option
("--workspace-root",
string
()), {
context
:
derived
.
context
,
key
: "workspaceRoot",
}), }); const
result
= await
runAsync
(
parser
, {
args
: ["--service-root", "/srv/api"],
contexts
: [
derived
.
context
],
});
console
.
log
(
result
.
workspaceRoot
); // "/srv/api/workspace"

Priority order

CLI arguments always win over derived values. If the resolver returns undefined, bindDerivedDefault() falls through to the static default value when one is provided:

import { 
object
} from "@optique/core/constructs";
import {
option
} from "@optique/core/primitives";
import {
string
} from "@optique/core/valueparser";
import {
runAsync
} from "@optique/run";
import {
bindDerivedDefault
,
createDerivedDefaults
,
} from "@optique/derived-defaults"; const
derived
=
createDerivedDefaults
({
token
: (
parsed
: { readonly
service
?: string }) =>
parsed
.
service
== null ?
undefined
: `${
parsed
.
service
}-token`,
}); const
parser
=
object
({
service
:
option
("--service",
string
()),
token
:
bindDerivedDefault
(
option
("--token",
string
()), {
context
:
derived
.
context
,
key
: "token",
default
: "local-token",
}), }); const
cliValue
= await
runAsync
(
parser
, {
args
: ["--service", "api", "--token", "manual"],
contexts
: [
derived
.
context
],
}); const
derivedValue
= await
runAsync
(
parser
, {
args
: ["--service", "api"],
contexts
: [
derived
.
context
],
});
console
.
log
(
cliValue
.
token
); // "manual"
console
.
log
(
derivedValue
.
token
); // "api-token"

Validation

Derived and static fallback values are validated through the wrapped parser. For example, a derived port number must still satisfy the integer() parser's range constraints:

import { 
object
} from "@optique/core/constructs";
import {
option
} from "@optique/core/primitives";
import {
integer
,
string
} from "@optique/core/valueparser";
import {
bindDerivedDefault
,
createDerivedDefaults
,
} from "@optique/derived-defaults"; const
derived
=
createDerivedDefaults
({
port
: (
parsed
: { readonly
profile
?: string }) =>
parsed
.
profile
== null
?
undefined
:
parsed
.
profile
=== "public"
? 443 : 8080, }); const
parser
=
object
({
profile
:
option
("--profile",
string
()),
port
:
bindDerivedDefault
(
option
("--port",
integer
({
min
: 1,
max
: 65535 })), {
context
:
derived
.
context
,
key
: "port",
}), });

Async resolvers

Resolvers may return promises. Use runAsync() or another async runner when a resolver is asynchronous:

import { 
object
} from "@optique/core/constructs";
import {
option
} from "@optique/core/primitives";
import {
string
} from "@optique/core/valueparser";
import {
runAsync
} from "@optique/run";
import {
bindDerivedDefault
,
createDerivedDefaults
,
} from "@optique/derived-defaults"; declare function
loadToken
(
service
: string):
Promise
<string>;
const
derived
=
createDerivedDefaults
({
token
: (
parsed
: { readonly
service
?: string }) =>
parsed
.
service
== null ?
undefined
:
loadToken
(
parsed
.
service
),
}); const
parser
=
object
({
service
:
option
("--service",
string
()),
token
:
bindDerivedDefault
(
option
("--token",
string
()), {
context
:
derived
.
context
,
key
: "token",
}), }); const
result
= await
runAsync
(
parser
, {
args
: ["--service", "api"],
contexts
: [
derived
.
context
],
});

Help defaults

Derived values are computed from runtime parse results, so help text cannot show the exact default value without parsing first. Use defaultDescription to document how the value is derived. It accepts any Optique Message, so you can reference option names or use styled message fragments:

import { 
object
} from "@optique/core/constructs";
import {
option
} from "@optique/core/primitives";
import {
message
,
optionName
} from "@optique/core/message";
import {
string
} from "@optique/core/valueparser";
import {
bindDerivedDefault
,
createDerivedDefaults
,
} from "@optique/derived-defaults"; const
derived
=
createDerivedDefaults
({
workspaceRoot
: (
parsed
: { readonly
serviceRoot
?: string }) =>
parsed
.
serviceRoot
== null
?
undefined
: `${
parsed
.
serviceRoot
}/workspace`,
}); const
parser
=
object
({
serviceRoot
:
option
("--service-root",
string
()),
workspaceRoot
:
bindDerivedDefault
(
option
("--workspace-root",
string
()), {
context
:
derived
.
context
,
key
: "workspaceRoot",
defaultDescription
:
message
`derived from ${
optionName
("--service-root")
}`, }), });

Composing with other sources

bindDerivedDefault() composes with other source wrappers. The outermost wrapper decides the higher-priority source, but wrapper-specific missing-value semantics still apply. For example, put bindEnv() outside bindDerivedDefault() when an environment variable should override the derived value:

import { 
object
} from "@optique/core/constructs";
import {
option
} from "@optique/core/primitives";
import {
integer
,
string
} from "@optique/core/valueparser";
import {
bindEnv
,
createEnvContext
} from "@optique/env";
import {
bindDerivedDefault
,
createDerivedDefaults
,
} from "@optique/derived-defaults"; const
env
=
createEnvContext
({
prefix
: "APP_" });
const
derived
=
createDerivedDefaults
({
port
: (
parsed
: { readonly
profile
?: string }) =>
parsed
.
profile
== null
?
undefined
:
parsed
.
profile
=== "public"
? 443 : 8080, }); const
parser
=
object
({
profile
:
option
("--profile",
string
()),
port
:
bindEnv
(
bindDerivedDefault
(
option
("--port",
integer
()), {
context
:
derived
.
context
,
key
: "port",
default
: 3000,
}), {
context
:
env
,
key
: "PORT",
parser
:
integer
(),
}, ), });

This parser resolves port as CLI > environment variable > derived default > static default. If you also use bindConfig(), place the default on the config wrapper when the configuration layer owns the missing-value fallback, or keep the wrappers separate for options whose fallback source should remain independent.