Record and Partial patterns for Forms
Type form values, errors, touched flags, and dirty maps with Record, Partial, and keyof so field names stay in sync.
- typescript
- record-and
Forms duplicate field names across values, errors, touched flags, and disabled maps. If those lists drift, TypeScript didn’t fail you — you modeled the form as freeform bags. Record, Partial, and keyof keep one field union everywhere.
Docs: Utility Types, keyof.
Single source: values type
type LoginForm = {
email: string;
password: string;
remember: boolean;
};
type FieldName = keyof LoginForm; // 'email' | 'password' | 'remember'
Errors and touched
type FormErrors<T> = Partial<Record<keyof T, string>>;
type Touched<T> = Partial<Record<keyof T, boolean>>;
const errors: FormErrors<LoginForm> = {
email: 'Enter a valid email',
};
const touched: Touched<LoginForm> = {
email: true,
};
Partial means not every field has an error — correct for validation UX.
Full boolean maps
When every key must be present (e.g. dirty flags after init):
type DirtyMap<T> = Record<keyof T, boolean>;
const dirty: DirtyMap<LoginForm> = {
email: false,
password: false,
remember: false,
};
Missing keys fail compile — good for resets.
Generic form state
type FormState<T> = {
values: T;
errors: Partial<Record<keyof T, string>>;
touched: Partial<Record<keyof T, boolean>>;
submitting: boolean;
};
const initial: FormState<LoginForm> = {
values: { email: '', password: '', remember: false },
errors: {},
touched: {},
submitting: false,
};
setField helper
function setField<T, K extends keyof T>(
state: FormState<T>,
key: K,
value: T[K],
): FormState<T> {
return {
...state,
values: { ...state.values, [key]: value },
touched: { ...state.touched, [key]: true },
};
}
Indexed access keeps value aligned with the field.
Validating to errors
function validate(values: LoginForm): FormErrors<LoginForm> {
const errors: FormErrors<LoginForm> = {};
if (!values.email.includes('@')) errors.email = 'Invalid email';
if (values.password.length < 8) errors.password = 'Min 8 characters';
return errors;
}
Partial updates (PATCH bodies)
type User = {
id: string;
name: string;
email: string;
};
type UserPatch = Partial<Omit<User, 'id'>>;
function updateUser(id: string, patch: UserPatch) {
return fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(patch),
});
}
Same Partial idea as optional form fields.
Record for lookup widgets
const fieldLabels: Record<keyof LoginForm, string> = {
email: 'Email',
password: 'Password',
remember: 'Remember me',
};
If you add a form field and forget the label map, compile fails.
React sketch
function Field({
name,
label,
value,
error,
onChange,
}: {
name: FieldName;
label: string;
value: string | boolean;
error?: string;
onChange: (name: FieldName, value: string | boolean) => void;
}) {
const id = String(name);
return (
<div>
<label htmlFor={id}>{label}</label>
<input
id={id}
value={typeof value === 'string' ? value : undefined}
checked={typeof value === 'boolean' ? value : undefined}
onChange={(e) =>
onChange(
name,
e.target.type === 'checkbox' ? e.target.checked : e.target.value,
)
}
/>
{error ? (
<p id={`${id}-err`} role="alert">
{error}
</p>
) : null}
</div>
);
}
Wire labels carefully for a11y — accessible forms errors.
Footguns
Record<string, string>for errors — allows any key, loses field sync.- Optional values type vs empty string — prefer always-defined controlled values.
- Boolean fields in the same Record as strings — unions get messy; split components by type.
- Deep forms —
Partialis shallow; nest explicitly.
Interview out-loud answer
“I define form values once, then Partial<Record<keyof T, string>> for errors and touched. Full Record<keyof T, boolean> when every flag must exist. Field helpers use K extends keyof T so setValue stays typed.”
Related on this site
- Utility types Partial Pick Omit
- keyof typeof and indexed access
- Mapped types intro
- Typing React props
- Accessible forms errors
Further reading
Related guides
- Basic Types and AnnotationsPrimitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Conditional Types IntroT extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- Declaration Files and DefinitelyTypedHow .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.
- Discriminated Unions for UI StateModel loading, success, and error as mutually exclusive variants so TypeScript and your UI cannot show impossible states.