Serialize
queryString.toQueryString(params) turns an object back into a query string. queryString.update(params) does the same and writes the result to window.location via history.replaceState.
Signatures
queryString.toQueryString(params: Record<string, any> | string): stringqueryString.update(params: Record<string, any> | string): voidqueryString.toString(): string // window.location.search.substring(1)Basics
queryString.toQueryString({}); // ""queryString.toQueryString({ foo: "bar" }); // "foo=bar"queryString.toQueryString({ a: 1, b: 2 }); // "a=1&b=2"queryString.toQueryString({ count: 5 }); // "count=5"Insertion order is preserved (Object.keys iterates string keys in insertion order for non-integer keys).
Primitives
queryString.toQueryString({ on: true }); // "on=true"queryString.toQueryString({ on: false }); // "on=false"queryString.toQueryString({ v: undefined }); // "v=undefined"queryString.toQueryString({ v: null }); // "v=null"null is short-circuited before the recursive object branch and emits the literal "null". undefined runs through encodeURIComponent and emits the literal "undefined". Filter either at the call site if you want the key dropped.
Arrays — key[]=value
queryString.toQueryString({ tags: [] }); // ""queryString.toQueryString({ tags: ["a"] }); // "tags[]=a"queryString.toQueryString({ tags: ["a", "b", "c"] }); // "tags[]=a&tags[]=b&tags[]=c"queryString.toQueryString({ ids: [1, 2, 3] }); // "ids[]=1&ids[]=2&ids[]=3"An empty array drops the key entirely (the inner .map(...).join("&") produces "").
Nested objects — parent[child]=value
queryString.toQueryString({ user: { name: "alice" } });// "user[name]=alice"
queryString.toQueryString({ user: { name: "alice", age: 30 } });// "user[name]=alice&user[age]=30"
queryString.toQueryString({ a: { b: { c: 1 } } });// "a[b][c]=1"String passthrough
If you already hold a query string, hand it through unchanged:
queryString.toQueryString("already=encoded"); // "already=encoded"queryString.toQueryString(""); // ""This is what makes queryString.update("foo=bar") work — the string short-circuits the serializer.
Percent-encoding
toQueryString calls encodeURIComponent on each value (and each array element), so reserved characters round-trip safely without pre-encoding at the call site:
queryString.toQueryString({ q: "a&b" }); // "q=a%26b"queryString.toQueryString({ q: "a=b" }); // "q=a%3Db"queryString.toQueryString({ q: "hello world" }); // "q=hello%20world"Keys are NOT encoded — only values. If a key itself contains reserved characters, that’s the caller’s problem; the public API expects regular identifier-shaped keys.
Pre-encoding values yourself (e.g. encodeURIComponent("a&b")) before passing them in will double-encode (%2526 instead of %26). Pass raw strings and let the serializer handle it.
update — write to the URL
queryString.update({ tag: "books", page: 3 });// URL becomes /products?tag=books&page=3
queryString.update("page=4");// URL becomes /products?page=4
queryString.update({});// URL becomes /products — empty object clears the queryCalls window.history.replaceState({}, "", url). Consequences:
- No new history entry is pushed — back-button keeps working as before.
- No
popstateevent fires. - No reload, no fetch of the page.
- The pathname is preserved; only the search string changes.
For a navigation that DOES push history, use history.pushState directly:
const qs = queryString.toQueryString({ page: 3 });history.pushState({}, "", `${location.pathname}?${qs}`);toString — read the URL
queryString.toString(); // "tag=books&page=2" — current ?... without the "?"Returns "" if there’s no query string. Cheap call.
Round-tripping
For values containing only safe characters, parse and serialize are inverses (modulo numeric coercion):
const obj = { tag: "books", page: 2, ids: [1, 2, 3] };const text = queryString.toQueryString(obj); // "tag=books&page=2&ids[]=1&ids[]=2&ids[]=3"queryString.parse(text); // structurally equal to `obj`Numeric-looking strings come back as numbers — { n: "42" } serializes to "n=42" which parses to { n: 42 }. That asymmetry is intentional: the URL format is ambiguous, the parser picks the most useful type.
Related skill cards
parse.mdfor the inverse direction.recipes.mdfor end-to-end flows.