Volume 4

Q212: Type Safe Forms, Validation and User Input

Difficulty: SeniorFrequency: HighAnswer time: 8-12 minutes

What Interviewers Want To Evaluate

Interviewers want to know whether you understand that TypeScript alone cannot validate user input.

They check form state modeling, schema validation, field errors, touched state, submit payloads, server validation, accessibility, progressive enhancement, and how type safety crosses runtime boundaries.

Short Interview Answer

I model forms with typed values, typed errors, and explicit submission states, but I do not rely on TypeScript to validate user input. User input is runtime data, so it needs runtime validation on the client for feedback and on the server for trust. Ideally, a schema or shared contract derives both the TypeScript type and the validation behavior so the form, API, and server stay aligned.

Detailed Interview Answer

Forms are one of the places where TypeScript boundaries matter most.

A form deals with:

user-entered strings
partial values
invalid values
touched fields
field-level errors
async submission
server rejection
accessibility feedback

Types help organize the workflow.

Validation proves runtime values.

Form Values

Start with the domain payload.

type SignupInput = {
  name: string;
  email: string;
  password: string;
};

But form state may be more detailed than the payload.

type SignupFormState = {
  values: SignupInput;
  errors: Partial<Record<keyof SignupInput, string>>;
  touched: Partial<Record<keyof SignupInput, boolean>>;
  status: "idle" | "submitting" | "success" | "error";
};

This keeps field-level metadata aligned with fields.

User Input Is Runtime Data

Every input value starts as a string.

<input value={age} onChange={(event) => setAge(event.currentTarget.value)} />

Even if the domain wants a number, the browser gives you text.

You need parsing.

function parseAge(value: string): number | null {
  const parsed = Number(value);
  return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
}

TypeScript cannot make the browser input valid.

Validation Shape

A simple validator can return a typed result.

type ValidationResult<T> =
  | { ok: true; data: T }
  | { ok: false; errors: Partial<Record<keyof T, string>> };

Example:

function validateSignup(values: SignupInput): ValidationResult<SignupInput> {
  const errors: Partial<Record<keyof SignupInput, string>> = {};

  if (!values.name.trim()) errors.name = "Name is required";
  if (!values.email.includes("@")) errors.email = "Enter a valid email";
  if (values.password.length < 8) errors.password = "Use at least 8 characters";

  return Object.keys(errors).length ? { ok: false, errors } : { ok: true, data: values };
}

In real applications, a validation library or schema system is often better.

Client And Server Validation

Client validation improves UX.

Server validation protects the system.

You need both.

client: fast feedback, disable obvious invalid submit, accessible messages
server: trusted validation, authorization, uniqueness checks, persistence rules

Never trust client validation alone.

The server must validate everything important.

Schema-Derived Types

Many teams use schema libraries to keep runtime validation and TypeScript types aligned.

Conceptually:

const signupSchema = createSchema({
  name: string().min(1),
  email: string().email(),
  password: string().min(8),
});

type SignupInput = Infer<typeof signupSchema>;

The exact library does not matter in an interview unless asked.

The important idea is that runtime validation and static types should not drift.

Submission State

Avoid many booleans.

type SubmitState =
  | { status: "idle" }
  | { status: "submitting" }
  | { status: "success"; message: string }
  | { status: "error"; message: string };

This prevents impossible combinations.

loading true and success true
error present while success message is shown
submit enabled while request is active

Discriminated unions are especially useful for form workflows.

Field Errors

Field errors should map to fields.

type FieldErrors<TValues> = Partial<Record<keyof TValues, string>>;

This catches typos.

const errors: FieldErrors<SignupInput> = {
  emial: "Invalid",
};

TypeScript flags emial.

Accessibility

Type-safe forms still need accessible feedback.

<input
  aria-invalid={Boolean(errors.email)}
  aria-describedby={errors.email ? "email-error" : undefined}
/>
{errors.email ? <p id="email-error">{errors.email}</p> : null}

Validation should be perceivable, not only stored in state.

Server Errors

Server errors may not map cleanly to one field.

type ServerFormError<TValues> =
  | { kind: "field"; errors: Partial<Record<keyof TValues, string>> }
  | { kind: "form"; message: string }
  | { kind: "auth"; message: string };

This helps the UI decide where to show the error.

Common Mistakes

Common mistakes include:

assuming TypeScript validates user input
using Partial payloads without clear submit requirements
trusting client validation only
using boolean-heavy form status
forgetting accessible error wiring
ignoring server-only validation rules
casting form data instead of parsing it

The biggest issue is confusing static types with runtime truth.

Senior Trade-Offs

For small forms, simple typed state and validators are fine.

For complex forms, prefer:

schema validation
field-level state helpers
server validation reuse
accessible error components
explicit submit state
typed API payloads

Avoid building a form framework unless the product truly needs one.

Interview Framing

In an interview, say:

TypeScript helps model form values and states, but validation is runtime work. I keep client and server validation aligned, model submit states with unions, and make errors accessible and field-specific where possible.

Revision Notes

  • Browser input is runtime data.
  • Client validation is UX; server validation is trust.
  • Field errors can be typed with Partial<Record<keyof T, string>>.
  • Submission states work well as discriminated unions.
  • Schema-derived types reduce drift.
  • Accessibility is part of form correctness.

Follow-Up Questions

  • Why is TypeScript not enough for form validation?
  • How would you type field errors?
  • What validation must run on the server?
  • How do discriminated unions improve form status?
  • How should accessible form errors be wired?

How I Would Answer This In A Real Interview

I would say that TypeScript is useful for form contracts, but user input is untrusted runtime data. I would model values, errors, touched state, and submit state explicitly, use runtime validation on client and server, and prefer schema-derived types when possible. I would also mention accessibility because a type-safe form that does not announce errors is still poor UX.