typescript 45 lines · 8 steps

Parsing bracketed form field names into nested objects

Turn HTML form fields like user[roles][] into a nested object by parsing their names and building the structure on the fly.

Explained by highlit
1type NestedValue = string | NestedValue[] | { [key: string]: NestedValue };
2 
3function parseFieldPath(name: string): string[] {
4 const match = name.match(/^([^\[\]]+)((?:\[[^\[\]]*\])*)$/);
5 if (!match) return [name];
6 const [, head, rest] = match;
7 const keys = [head];
8 for (const segment of rest.matchAll(/\[([^\[\]]*)\]/g)) {
9 keys.push(segment[1]);
10 }
11 return keys;
12}
13 
14function assign(target: Record<string, NestedValue>, keys: string[], value: string): void {
15 let cursor: any = target;
16 for (let i = 0; i < keys.length - 1; i++) {
17 const key = keys[i];
18 const nextKey = keys[i + 1];
19 const isArray = nextKey === '' || /^\d+$/.test(nextKey);
20 if (cursor[key] == null) {
21 cursor[key] = isArray ? [] : {};
22 }
23 cursor = cursor[key];
24 }
25 
26 const last = keys[keys.length - 1];
27 if (last === '' && Array.isArray(cursor)) {
28 cursor.push(value);
29 } else {
30 cursor[last] = value;
31 }
32}
33 
34export function serializeForm(form: HTMLFormElement): Record<string, NestedValue> {
35 const result: Record<string, NestedValue> = {};
36 const data = new FormData(form);
37 
38 for (const [name, raw] of data.entries()) {
39 if (typeof raw !== 'string') continue;
40 const keys = parseFieldPath(name);
41 assign(result, keys, raw);
42 }
43 
44 return result;
45}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1A recursive type like NestedValue lets you describe arbitrarily deep mixes of objects and arrays in one definition.
  2. 2Peeking at the next key decides whether to create an array or object, so structure emerges from the path shape itself.
  3. 3Splitting name-parsing from value-assignment keeps each function small and independently testable.

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
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
ruby
class UserAgentParser
  BROWSERS = [
    [/Edg\/([\d.]+)/, "Edge"],
    [/OPR\/([\d.]+)/, "Opera"],

Parsing user-agent strings in Ruby

regex pattern-matching lookup-tables
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
typescript
import { Injectable, effect, signal, computed } from '@angular/core';
 
interface Preferences {
  theme: 'light' | 'dark';

A signal-based preferences store in Angular

signals state-management persistence
Intermediate 7 steps

Share this explainer

Here's the card — post it anywhere.

Parsing bracketed form field names into nested objects — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code