conform-to-effect
v1.0.3
Published
Conform helpers for integrating with Effect.
Maintainers
Readme
conform-to-effect
Conform helpers for integrating with Effect.
Install
npm install effect @conform-to/dom @conform-to/react conform-to-effectQuick Start
import { Result, Schema } from "effect"
import { coerceFormValue, formatResult } from "conform-to-effect"
const schema = coerceFormValue(
Schema.Struct({
age: Schema.optional(Schema.Number),
subscribed: Schema.Boolean,
}),
)
const result = Schema.decodeUnknownResult(schema, { errors: "all" })({
age: "42",
subscribed: "on",
})
if (Result.isSuccess(result)) {
result.success
// { age: 42, subscribed: true }
}
formatResult(result)Prefer configureForms for less boilerplate:
import { configureForms } from "@conform-to/react/future"
import { Schema, SchemaAST } from "effect"
import {
coerceFormValue,
formatResult,
getConstraints,
isSchema,
} from "conform-to-effect"
// Augment Conform's type inference for Effect schemas
declare module "@conform-to/react/future" {
interface CustomSchemaTypes<Schema> {
input: Schema extends { readonly Encoded: infer E } ? E : never
output: Schema extends { readonly Type: infer T } ? T : never
options: SchemaAST.ParseOptions
}
}
const {
FormProvider,
useField,
useForm,
useFormMetadata,
useIntent,
} = configureForms({
isSchema,
getConstraints,
validateSchema(schema, payload) {
const result = Schema.decodeUnknownResult(
coerceFormValue(schema),
{ errors: "all" },
)(payload)
return formatResult(result, {
includeValue: true,
})
},
})
const signupSchema = Schema.Struct({
email: Schema.String,
age: Schema.optional(Schema.Number),
subscribed: Schema.Boolean,
})
function SignupForm() {
const { form, fields } = useForm(signupSchema, {
defaultValue: {
email: "[email protected]",
subscribed: true,
},
})
return (
<form id={form.id} onSubmit={form.onSubmit} noValidate>
<input name={fields.email.name} defaultValue={fields.email.defaultValue} />
<input name={fields.age.name} defaultValue={fields.age.defaultValue} />
<input
type="checkbox"
name={fields.subscribed.name}
defaultChecked={fields.subscribed.defaultChecked}
/>
</form>
)
}API
Exports:
coerceFormValuecoerceStructureconfigureCoercionformatResultformatExitgetConstraintsisSchema
coerceFormValue
Wrap an Effect schema so raw form values are coerced before normal validation runs.
- Strips empty strings to
undefined - Coerces strings into
number,boolean,date, andbigint - Still runs the original schema validation after coercion
import { Schema } from "effect"
import { coerceFormValue } from "conform-to-effect"
const schema = coerceFormValue(
Schema.Struct({
age: Schema.optional(Schema.Number),
subscribed: Schema.Boolean,
createdAt: Schema.Date,
}),
)
const value = Schema.decodeUnknownSync(schema)({
age: "",
subscribed: "on",
createdAt: "2026-01-01T12:00:00.000",
})
// { age: undefined, subscribed: true, createdAt: new Date("2026-01-01T12:00:00.000Z") }coerceStructure
Read current form data as typed structure without running validation rules.
- Coerces strings into typed values
- Skips validation, defaults, and transforms
- Uses fallback values like
NaN,false,Invalid Date, and0nwhen coercion fails
import { Schema } from "effect"
import { coerceStructure } from "conform-to-effect"
const schema = coerceStructure(
Schema.Struct({
title: Schema.String,
age: Schema.Number,
subscribed: Schema.Boolean,
}),
)
const value = Schema.decodeUnknownSync(schema)({
title: "",
age: "abc",
subscribed: "",
})
// { title: "", age: NaN, subscribed: false }configureCoercion
Create customized coercion helpers for project-specific string handling.
- Override empty-string stripping
- Override string-to-
number/boolean/datecoercion - Returns configured
coerceFormValueandcoerceStructure customizecan override coercion at the wrapped schema boundary
import { Result, Schema } from "effect"
import { configureCoercion } from "conform-to-effect"
const { coerceFormValue } = configureCoercion({
stripEmptyString: (value) => {
const trimmed = value.trim()
return trimmed === "" ? undefined : trimmed
},
type: {
number: (text) => Number(text.replace(/,/g, "")),
},
})
const schema = coerceFormValue(
Schema.Struct({
price: Schema.Number,
}),
)
const result = Schema.decodeUnknownResult(schema)({
price: "1,234",
})
if (Result.isSuccess(result)) {
result.success.price
// 1234
}formatResult
Convert an Effect decode result into Conform's form error shape.
- Success returns
nullby default - Failure becomes
{ formErrors, fieldErrors } includeValue: truealso returns the parsed value
import { Schema } from "effect"
import { coerceFormValue, formatResult } from "conform-to-effect"
const schema = coerceFormValue(
Schema.Struct({
age: Schema.Number,
}),
)
const result = Schema.decodeUnknownResult(schema)({
age: "abc",
})
const submission = formatResult(result)
// {
// formErrors: null,
// fieldErrors: { age: ['Expected number, got "abc"'] }
// }getConstraints
Derive native HTML validation attributes from an Effect schema.
- Returns field constraints like
required,minLength,maxLength,min,max, andpattern - Useful for wiring schema constraints into Conform metadata
- Returns
undefinedfor non-schema input
import { Schema } from "effect"
import { getConstraints } from "conform-to-effect"
const Article = Schema.Struct({
title: Schema.String.check(Schema.isMinLength(5), Schema.isMaxLength(20)),
})
const constraints = getConstraints(Article)
// { title: { required: true, minLength: 5, maxLength: 20 } }isSchema
Check whether a value is an Effect schema.
- Returns
truefor valid Effect schemas - Narrows unknown values to schema values
- Handy before calling
getConstraintson dynamic input
import { Schema } from "effect"
import { getConstraints, isSchema } from "conform-to-effect"
const value: unknown = Schema.String
if (isSchema(value)) {
const constraints = getConstraints(value)
}