Codec & modifiers
The Codec interface, the three modifiers, and the type-state rules.
Every value paramour moves across the URL boundary is described by a codec.
This page documents the Codec interface itself, the three chainable
modifiers, and the type-state rules that make
illegal modifier chains fail to compile. For the catalog of built-in
builders, see p.* builders; for the conceptual tour, see
Codecs & the type-state API.
Codec
The interface behind every builder: Codec<Out, P, C, A, E>, a
bidirectional wire codec whose modifier history is tracked in its type
parameters.
Out— the decoded in-memory type (numberforp.integer(),Dateforp.isoDate()).P(presence) —"required","optional", or"defaulted". Presence governs absence semantics on both sides: whether the key may be omitted when building a URL, and whether the decoded property can beundefined.C(caught) —trueonce.catch()has been applied.A(arity) —"single"(one wire value per key) or"many"(repeated keys; onlyp.arrayproduces this).E(default-elides) —trueafter a value-form.default(),falseafter a factory-form one, unresolvedbooleanbefore either. Derived surfaces (the nuqs adapter) read this literal to decide whether a defaulted key can be given a non-nullable read.
The type parameters are type-state: modifier methods are conditionally
never, so an illegal chain makes the method itself uncallable. Hover the
chain to watch the state accumulate:
import { } from "paramour";
const page = .().(1).(0);Only Out is stable API: the type-state parameters after it may gain
members or change shape in a minor release, so don't spell them out —
write AnyCodec<number> for "a codec producing numbers in any state", and
read state through the utility types below.
Properties prefixed ~ (~kind, ~presence, ~parseElement, …) are
internal machinery, not public API and not covered by semver — user code
reads codecs through describeCodec and the
utility types below. ~out in particular is a phantom property: it
carries Out for inference and is never set at runtime.
.optional()
Marks the key as allowed to be absent.
The decoded type gains | undefined, the key becomes omittable on the
href/encode side, and serializing undefined omits the key entirely.
import { , } from "paramour";
const = { : .().() };
const search = (, new (""));Available only on "required"-presence, "single"-arity codecs — see
type-state rules.
.default(value | factory)
Substitutes a value when the key is absent — and, in the value form, elides the key when serializing a value equal to the default.
The two forms differ deliberately (they are distinguished in type-state via
the E parameter):
import { } from "paramour";
const page = .().(1);
const since = .().(() => new ());- Value form (
.default(1)) — absent decodes as1, and serializing1elides the key, so every state has exactly one canonical URL (no?page=1and?aliases). The comparison is by wire form, and the live default is re-serialized per encode — never a stale snapshot — so mutating a reference-typed default cannot desync encode from decode. The default is also serialized once at definition time, so a default the codec cannot serialize (or that fails its schema) throws aParamourErrorwhen the config is defined, not on some later encode. - Factory form (
.default(() => new Date())) — the factory runs per decode, so reference-typed defaults are isolated per call. Factory defaults never elide: a time-varying default could swallow an explicitly-passed value that later decodes as a different one.
Array values passed to .default() are handed out as fresh shallow copies
per call, so mutating a decoded fallback can't pollute later decodes; other
reference values are returned by reference — use a factory to isolate
those.
.catch(fallback | factory)
Recovers a failed parse to a fallback value instead of failing the decode.
import { , } from "paramour";
const = { : .(["grid", "list"]).("grid") };
const = (, new ("?view=nonsense"));
// => { view: "grid" }.catch() is orthogonal to presence: it recovers parse failures, never
absence — an absent optional key is still undefined, an absent required
key is still an error. It also only recovers data-level ParseErrors:
misconfiguration (ParamourError) and serialization bugs
(SerializeError) stay loud. Like .default(), it accepts a value or a
zero-arg factory (array values are shallow-copied per call), and its
argument is typed to the codec's output:
import { } from "paramour";
.(["asc", "desc"]).("descending");An .optional() codec decodes to Out | undefined, so it may also recover
to absent. Use this when a malformed value should read as "nothing
selected" and no in-domain value means that:
import { , } from "paramour";
const = { : .(["gold", "silver"]).().() };
const = (, new ("?tier=bronze"));
// => { tier: undefined }Apply .optional() first. .catch(undefined) on a required or defaulted
codec fails to compile, and throws a ParamourError from plain JavaScript.
Type-state rules
Contradictory chains fail to compile: applying a modifier removes it (and
any modifier it excludes) from the resulting type, collapsing the method to
never. Calling a never-typed method is TS error 2349:
import { } from "paramour";
.().().optional();
.().().default("a");
.().("a").optional();
.().(1).default(2);
.().(0).catch(1);The rules, spelled out:
- Each modifier may be applied once.
.optional()and.default()are mutually exclusive in either order — what would.optional().default(1)mean when the key is absent?.catch()combines with either presence modifier, in any order:p.integer().catch(0).default(1)andp.integer().default(1).catch(0)are both legal. The one exception is anundefinedfallback, which only an optional key can recover to, so it needs.optional()first:p.integer().optional().catch(undefined).- Arity-
"many"codecs (p.array) have no presence modifiers at all — absent and[]are the same wire state (seep.array).
Mismatched modifier arguments fail like ordinary typed calls — for
.default(), whose value/factory split is expressed as overloads, a
wrong-typed argument reports error 2769:
import { } from "paramour";
.().("x");One subtlety of the overloads: an Out whose static type includes a
function member can only be defaulted through the factory form — the
runtime treats any function argument as a factory, so the value branch is
withheld whenever the two could disagree.
These rules are not types-only theater: the same illegal chains throw
ParamourError at runtime for plain-JavaScript consumers. The type-state
is a compile-time mirror of real constraints.
Related types
AnyCodec<Out = any>— any codec, in any presence/catch/arity state;AnyCodec<number>narrows it to codecs producingnumber. The constraint to use when writing generic code over codecs.CodecKind— which builder produced a codec ("integer","csv","custom", …), as reported bydescribeCodec.InferCodecOutput<C>— the decoded type of a codec:InferCodecOutput<ReturnType<typeof p.integer>>isnumber.ParamCodec— the codecs legal in a route'sparams:config: no presence modifiers (a path segment can't be absent),.catch()allowed.Presence—"defaulted" | "optional" | "required".PresenceOf<C>— a codec's presence state as a literal type.Arity—"many" | "single".
import { } from "paramour";
import type { , } from "paramour";
const = .().(1);
type Page = <typeof >;
type PagePresence = <typeof >;