Typorithm · hard

8. Forms, FormData and validated submission

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

8. Forms, FormData and validated submission

0%

Concept and contract

Convert user-entered text into a domain command before sending it.

A form field value is external input even when the form component is typed. FormData.get can return a string, a File or null. A text field name in JSX does not narrow every later FormData lookup to string, so validate the actual returned value.

Separate parsing from submission. A pure parser can report field errors and produce a trusted command; the UI handles pending state and displays accessible feedback. This makes the core behavior testable without a browser and keeps network policy out of small field handlers.

Mechanism and worked example

The example parses a page-size text value into a bounded integer.

The example parses a page-size text value into a bounded integer. Empty text is rejected explicitly before Number conversion, because Number of empty text is zero. The parser chooses its accepted syntax rather than assuming type=number established a valid domain value.

A form submit handler should prevent default navigation only when implementing client-side submission, read current form values, and pass them through the parser. Server-side validation remains mandatory because client code and HTML constraints can be bypassed.

Complete worked example

typescript

function parsePageSize(raw:unknown):number|undefined{
  if(typeof raw !== "string" || raw.trim() === "")return undefined;
  const value=Number(raw);return Number.isInteger(value)&&value>=1&&value<=100?value:undefined;
}
Reasoning
The parser defines accepted text, numeric bounds and absence behavior explicitly.

Counterexamples and common mistakes

The rejected assignment treats a FormData result as a guaranteed string.

The rejected assignment treats a FormData result as a guaranteed string. A File or missing value would violate that assumption. Casting it to string does not perform conversion or validate the field’s presence and meaning.

Do not disable the only error feedback while a request is pending, and do not announce success before the server accepts the command. Static types cannot guarantee focus movement, double-submit prevention or accessible error association; those require interaction tests.

Rejected counterexample: expected compiler diagnostic

typescript

const data = new FormData();
// @ts-expect-error FormData can return File or null as well as string.
const title:string=data.get("title");
Reasoning
The DOM API’s result must be narrowed before string operations.

Independent practice

Implement parseYears for unknown input from a text field.

Implement parseYears for unknown input from a text field. Accept only a nonempty decimal integer string from zero to fifty. Return a tagged result with either years or a field error. Begin with the empty case, then syntax, then range.

Test zero, leading whitespace according to your policy, fractions, text and out-of-range values. Optional extension: connect the parser to a labeled React form. The basic exercise remains a small pure parser with clear accepted syntax.

Step 1: Check nonempty decimal syntax. Step 2: Validate the zero-to-fifty range.

Practice starter: solve before reading the solution

typescript

type Years={ok:true;years:number}|{ok:false;error:string};
function parseYears(raw:unknown):Years{throw new Error("TODO");}
Reasoning
Build the parser in three small steps: presence, syntax and range.
  • Hint 1: Trim according to the documented input policy.
  • Hint 2: A typed form still accepts external user input.

Solution, tests and trade-offs

The solution trims input, requires digits and checks the numeric range.

The solution trims input, requires digits and checks the numeric range. Leading zeros are accepted deliberately; signs and exponent notation are rejected. These choices are part of the form contract and should be explained rather than hidden behind Number alone.

Run the boundary table and compile a caller that narrows the result before reading years. A browser harness should then verify error display and successful submission. Parsing correctness and interaction correctness are related but separate evidence.

Practice solution and acceptance checks

typescript

type Years={ok:true;years:number}|{ok:false;error:string};
function parseYears(raw:unknown):Years{
  if(typeof raw !== "string" || !/^\d+$/.test(raw.trim()))return {ok:false,error:"Enter whole years"};
  const years=Number(raw.trim());return years<=50?{ok:true,years}:{ok:false,error:"Use 0 to 50 years"};
}

// Acceptance checks
for(const x of ["0"," 2 ","50"])if(!parseYears(x).ok)throw new Error("valid");for(const x of ["","2.5","abc","51",null])if(parseYears(x).ok)throw new Error("invalid");
Reasoning
The solution accepts a documented decimal syntax and preserves zero as valid.

Interview reasoning and next step

In interviews, explain why a typed form is still an untrusted boundary.

In interviews, explain why a typed form is still an untrusted boundary. Describe FormData’s union, numeric syntax choices and client-versus-server validation. Include one accepted zero case so truthiness does not quietly discard a valid value.

Next, use context for shared state only where it has a clear owner. A form’s local errors do not automatically belong in a global context merely because several components exist on the page.

Optional stretch — after the core task works: Connect field errors to a labeled form interaction.

Convert user-entered text into a domain command before sending it.