coerce-form-data
Zero-dependency form data coercion.
Converts raw form values (strings) into properly typed JavaScript values (numbers, booleans, dates, etc.) and back again. Supports recursive structures: arrays, objects, and arbitrary nesting.
Features
- Zero dependencies
- Recursive: arrays of objects, nested objects, arrays of arrays
- Full TypeScript support with inferred return types
- ESM and CommonJS builds
- Reversible: coerce form strings to typed values and typed values back to form strings
- Throws
FormDataCoercionError on invalid values — no silent NaN or Invalid Date
Installation
npm install coerce-form-data
pnpm add coerce-form-data
yarn add coerce-form-data
Quick Start
import { coerceValue } from 'coerce-form-data'
coerceValue('42', { type: 'number' })
coerceValue('true', { type: 'boolean' })
coerceValue('2024-05-06', { type: 'date' })
coerceValue(['1', '2', '3'], { type: 'array', item: { type: 'number' } })
coerceValue(
{ name: 'Jane', age: '30', active: 'true' },
{
type: 'object',
fields: {
name: { type: 'string' },
age: { type: 'number' },
active: { type: 'boolean' },
},
},
)
coerceValue(
{ departments: [{ name: 'Eng', headcount: '50' }] },
{
type: 'object',
fields: {
departments: {
type: 'array',
item: {
type: 'object',
fields: {
name: { type: 'string' },
headcount: { type: 'number' },
},
},
},
},
},
)
Return types are automatically inferred from the field descriptors — no casts needed.
With composable-functions
Use composable-functions' inputFromForm to extract raw values from FormData (handling bracket notation, repeated keys, etc.), then pass the result to coerceValue for type coercion:
import { inputFromForm } from 'composable-functions'
import { coerceValue } from 'coerce-form-data'
const descriptor = {
type: 'object',
fields: {
name: { type: 'string' },
age: { type: 'number' },
},
} as const
const raw = inputFromForm(formData)
const typed = coerceValue(raw, descriptor)
API
coerceValue(value, field?)
Coerce a raw value into its typed representation. When no field descriptor is provided, the value is returned as-is.
coerceValue('42', { type: 'number' })
coerceValue('true', { type: 'boolean' })
coerceValue('2024-05-06', { type: 'date' })
coerceValue('2024-05-06T14:30', { type: 'datetime' })
coerceValue('hello', { type: 'string' })
coerceValue(file, { type: 'file' })
coerceValue(['1', '2'], { type: 'array', item: { type: 'number' } })
coerceValue(
{ street: 'Main St', zip: '12345' },
{ type: 'object', fields: { street: { type: 'string' }, zip: { type: 'number' } } },
)
Throws FormDataCoercionError for invalid values. For nested structures, the error includes a path indicating where the failure occurred.
coerceToForm(value, field)
The reverse of coerceValue. Converts a typed JavaScript value into the string (or boolean) that an HTML form input expects. Works recursively for arrays and objects.
coerceToForm(42, { type: 'number' })
coerceToForm(true, { type: 'boolean' })
coerceToForm(new Date('2024-05-06T12:00:00Z'), { type: 'date' })
coerceToForm(new Date(2024, 4, 6, 14, 30), { type: 'datetime' })
coerceToForm(file, { type: 'file' })
coerceToForm([1, 2], { type: 'array', item: { type: 'number' } })
coerceToForm(
{ name: 'Jane', age: 30 },
{ type: 'object', fields: { name: { type: 'string' }, age: { type: 'number' } } },
)
parseDate(value?)
Formats a Date or ISO date-time string as a YYYY-MM-DD string suitable for <input type="date">. Uses local time for Date instances.
parseDate(new Date(2024, 4, 6))
parseDate('2024-05-06T12:00:00Z')
parseDate(undefined)
parseDatetime(value?)
Formats a Date as a YYYY-MM-DDTHH:mm:ss string suitable for <input type="datetime-local">. Uses local time.
parseDatetime(new Date(2024, 4, 6, 14, 30, 45))
parseDatetime('2024-05-06T14:30')
parseDatetime(undefined)
FormDataCoercionError
Thrown when a value cannot be coerced to the declared field type. Extends Error with value, fieldType, and path properties.
For nested structures, the path array indicates where the error occurred:
import { coerceValue, FormDataCoercionError } from 'coerce-form-data'
try {
coerceValue(
{ departments: [{ name: 'Eng', headcount: 'bad' }] },
{
type: 'object',
fields: {
departments: {
type: 'array',
item: {
type: 'object',
fields: {
name: { type: 'string' },
headcount: { type: 'number' },
},
},
},
},
},
)
} catch (error) {
if (error instanceof FormDataCoercionError) {
error.value
error.fieldType
error.path
error.message
}
}
Field Descriptors
FieldDescriptor is a recursive discriminated union:
type ScalarFieldDescriptor = {
type: 'string' | 'number' | 'boolean' | 'date' | 'datetime' | 'enum' | 'file' | null
optional?: boolean
nullable?: boolean
}
type ArrayFieldDescriptor = {
type: 'array'
item: FieldDescriptor
optional?: boolean
nullable?: boolean
}
type ObjectFieldDescriptor = {
type: 'object'
fields: Record<string, FieldDescriptor>
optional?: boolean
nullable?: boolean
}
type FieldDescriptor = ScalarFieldDescriptor | ArrayFieldDescriptor | ObjectFieldDescriptor
This mirrors schema-info's SchemaInfo discriminated union, so a SchemaInfo value is structurally compatible as a FieldDescriptor.
Coercion behavior by type
string | 'hello' | 'hello' |
string | falsy | '' |
number | '42' | 42 |
number | falsy / invalid | throws FormDataCoercionError |
boolean | 'on', 'true', truthy | true |
boolean | 'false' | false |
boolean | 'null' (required) | throws FormDataCoercionError |
boolean | 'null' (nullable) | null |
boolean | 'null' (optional) | undefined |
boolean | falsy | false |
date | '2024-05-06' | Date(2024, 4, 6) |
date | falsy / invalid | throws FormDataCoercionError |
datetime | '2024-05-06T14:30' | Date(2024, 4, 6, 14, 30) |
datetime | falsy / invalid | throws FormDataCoercionError |
enum | 'value' | 'value' |
enum | falsy | '' |
file | File instance | File (pass-through) |
file | falsy / non-File | throws FormDataCoercionError |
array | ['1', '2'] with item: { type: 'number' } | [1, 2] |
array | falsy | [] |
object | { age: '30' } with fields: { age: { type: 'number' } } | { age: 30 } |
object | falsy | recurse with {} (inner fields get defaults or throw) |
Missing values
When a field value is missing (falsy), the optional and nullable flags control what is returned:
| (none) | Type-specific empty value (see table above), or throws for number/date/datetime |
optional: true | undefined |
nullable: true | null |
| both | null (nullable takes precedence) |
These flags work at every nesting level. An optional array returns string[] | undefined. An array of optional strings returns (string | undefined)[].
Type Inference
All functions infer return types from the field descriptors you pass:
coerceValue('42', { type: 'number' })
coerceValue('42', { type: 'number', optional: true })
coerceValue(['1'], { type: 'array', item: { type: 'number' } })
coerceValue({}, {
type: 'object',
fields: {
name: { type: 'string' },
age: { type: 'number' },
},
})
The utility types CoercedFieldValue and CoercedToFormValue are also exported for use in your own code.
Types
All types are exported:
import type {
ScalarFieldType,
ScalarFieldDescriptor,
ArrayFieldDescriptor,
ObjectFieldDescriptor,
FieldDescriptor,
FormValue,
CoercedFieldValue,
CoercedToFormValue,
} from 'coerce-form-data'
License
MIT