typescript
32 lines · 8 steps
Building nested query strings recursively
A recursive encoder flattens arrays and nested objects into a URL-safe query string.
Explained by
highlit
1type QueryValue = string | number | boolean | null | undefined;
2type QueryInput = QueryValue | QueryValue[] | { [key: string]: QueryInput };
3
4function buildQueryString(params: Record<string, QueryInput>): string {
5 const pairs: string[] = [];
6
7 const encode = (key: string, value: QueryInput): void => {
8 if (value === null || value === undefined) return;
9
10 if (Array.isArray(value)) {
11 for (const item of value) {
12 encode(`${key}[]`, item);
13 }
14 return;
15 }
16
17 if (typeof value === "object") {
18 for (const [childKey, childValue] of Object.entries(value)) {
19 encode(`${key}[${childKey}]`, childValue);
20 }
21 return;
22 }
23
24 pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
25 };
26
27 for (const [key, value] of Object.entries(params)) {
28 encode(key, value);
29 }
30
31 return pairs.length ? `?${pairs.join("&")}` : "";
32}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A recursive type mirrors a recursive function — the type describes exactly what the encoder can flatten.
- 2Recursion turns nested arrays and objects into flat key paths without special-casing depth.
- 3Deferring encodeURIComponent to leaf values keeps bracket syntax in keys readable while still escaping user data.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
rust
use axum::{ extract::{Path, State}, response::sse::{Event, KeepAlive, Sse}, };
Streaming import progress with SSE in Axum
server-sent-events
streams
watch-channel
Advanced
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-nested-query-strings-recursively-explained-typescript-2912/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.