Custom Presets
When built-in presets don’t cover your use case, you have two options:
- Plain config — inline
decode/encode/resolvefor one-off usage createPreset— reusable preset function with{ optional },{ default },{ array }options
Both demos below use enum validation (same pattern as z.enum() from Zod v4) and decimal.js for precise arithmetic. View the source code to see the full implementation.
Plain config (inline)
Section titled “Plain config (inline)”Pass a config object directly. Uses resolve for Decimal input binding — $value holds the string (bindable to <input>), $resolved holds the Decimal (for calculation). Clearing an input sets $value to "", and resolve maps it to the default.
{
sort: 'price_asc',
'price.$value': '',
'price.$resolved': Decimal(0),
'tax.$value': '',
'tax.$resolved': Decimal(0.1),
'total (price × (1+tax))': Decimal(0)
}(empty)
createPreset (reusable)
Section titled “createPreset (reusable)”Build preset functions with createPreset. The returned function accepts { optional }, { default }, { array } — the same API as built-in presets. Here enumPreset and decimal are defined once and reused across stores.
{
sort: 'price_asc',
'price.$value': '',
'price.$resolved': Decimal(0),
'tax.$value': '',
'tax.$resolved': Decimal(0.1),
'total (price × (1+tax))': Decimal(0)
}(empty)
Config shape
Section titled “Config shape”| Property | Type | Description |
|---|---|---|
decode | (v: unknown) => T | Decode URL string to typed value |
defaultValue | T | Value when parameter is missing |
encode | (v: T) => string | undefined | Encode back to URL (undefined = omit) |
resolve | (v: T) => R | Optional: map $value → $resolved |
isArray | boolean | Treat as array parameter |
createPreset options
Section titled “createPreset options”| Option | Description |
|---|---|
() | Base — uses the decode, defaultValue, and encode you provided |
{ optional: true } | Omits defaultValue, type becomes T | undefined |
{ default: value } | Override defaultValue |
{ array: true } | Array variant with per-item decode/encode |
{ array: true, maxItems: n } | Array capped at n items |
Note: createPreset does not support outOfRange, min/max, or numInput — those are specific to built-in integer() and float() presets. Bake constraint logic into your decode function instead.
Narrowing resolve
Section titled “Narrowing resolve”resolve can narrow the value type. For example, a string store where $resolved is a literal union:
import { createPreset } from "@vp-tw/nanostores-qs/presets";
type SortDir = "asc" | "desc";
const sortDirection = createPreset<string, string, SortDir>({ decode: (v) => String(v), defaultValue: "", resolve: (s): SortDir => (s === "desc" ? "desc" : "asc"),});
const store = qsUtils.createSearchParamStore("sort", sortDirection());// store.$value — ReadableAtom<string>// store.$resolved — ReadableAtom<"asc" | "desc">The resolve field is correctly inferred as required (not optional) whenever TResolved differs from TValue, including subtype narrowing like string → "asc" | "desc".
When to use what
Section titled “When to use what”| Scenario | Approach |
|---|---|
| Standard type (int, enum, boolean…) | Built-in preset (e.g. presets.integer()) |
| Custom logic, one place | Plain config object |
| Custom logic, reused across stores | createPreset |
Need $value ≠ $resolved (input binding) | Add resolve to config |