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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A recursive type like NestedValue lets you describe arbitrarily deep mixes of objects and arrays in one definition.
- 2Peeking at the next key decides whether to create an array or object, so structure emerges from the path shape itself.
- 3Splitting name-parsing from value-assignment keeps each function small and independently testable.
Related explainers
typescript
import sanitizeHtml from "sanitize-html"; interface RichTextOptions { allowImages?: boolean;
Building a configurable HTML sanitizer allowlist
sanitization
xss-prevention
allowlist
Intermediate
7 steps
typescript
import { useCallback, useEffect, useRef, useState } from "react"; interface Page<T> { items: T[];
A cursor-based infinite scroll hook in React
custom-hooks
pagination
intersection-observer
Intermediate
9 steps
typescript
import { Component } from '@angular/core'; import { NgForm } from '@angular/forms'; interface SignupModel {
How template-driven forms validate in Angular
forms
two-way-binding
validation
Intermediate
9 steps
rust
#[derive(Debug, PartialEq)] enum State { FieldStart, InUnquoted,
Parsing a CSV line with a state machine
state machine
parsing
enums
Intermediate
9 steps
typescript
import { Injectable, signal, computed } from '@angular/core'; export type ToastKind = 'success' | 'error' | 'info' | 'warning';
Building a signal-based toast service in Angular
signals
state-management
dependency-injection
Intermediate
8 steps
typescript
import { Controller } from '@nestjs/common'; import { MessagePattern, Payload,
Manual RabbitMQ acks in a NestJS controller
microservices
message-queue
acknowledgement
Intermediate
8 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/parsing-bracketed-form-field-names-into-nested-objects-explained-typescript-e3b7/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.