;tldr
Has anyone successfully used zod
for server side validation in Remix while using the new(ish) type inference for the action function? const actionData = useActionData<typeof action>()
. I can get it to work when I am only handling a single form action, but when handling multiple actions with different zod
signatures I am having issues parsing the errors back in the component.
Background
Up until now I have been blindly using useActionData()
and useLoaderData()
without any explicit type casting, but recently ran into a need to pass the action data down to a child component and wanted to get better autocomplete. This led me down a rabbit hole of issues.
I started on Remix v1.6.4
(prior to implementation of typeof loader
). After looking through the more recent releases, decided I would update to latest v1.7.2
to get the inferred types and now have a slew of errors.
For simplicity's sake, I am handling multiple for actions on a single route and using Zod
to server side validate. Any errors are formatted and returned, then consumed by the original elements to display the errors and keep the form inputs in sync. I believe my TS issue has to do with the error object that is returned by zod
.
Code
export async function action({request}: ActionArgs) {
const form = await request.formData()
let { _action, ...values } = Object.fromEntries(form.entries())
const FormInputs = z.object({
some_string: z.string().min(1),
some_enum: z.enum(['enum 1', 'enum 2']),
some_date: z.date(),
some_bool: z.boolean(),
})
const validate = FormInputs.safeParse(values)
if (!validate.success) {
const errors = validate.error.format()
return json({fields: values, errors}, {status: 400})
}
try {
// do some async stuff
} catch(error) {
// do something with error
}
return redirect('/some/redirect')
}
export default function RouteComponent() {
const actionData = useActionData<typeof action>()
function hasErrors(input: string) {
return {
flag: actionData?.errors?.[input as keyof typeof actionData.errors] ? true : false,
message: actionData?.errors?.[input as keyof typeof actionData.errors]._errors[0] // <-- This throws error
}
}
return <form method="post">...Some form components</form>
}
This methodology was working fine before updating to v1.7.2
and adding type inference. The error thrown is: Property '_errors' does not exist on ...[long type mostly of zod errors]
The exact type when I hover actionData
is:
const actionData: SerializeObject<Simplify<{
fields: {
[k: string]: FormDataEntryValue;
};
errors: z.ZodFormattedError<{
some_string: string;
some_enum: "enum 1" | "enum 2" | "enum 3";
some_date: Date;
some_bool: boolean;
}, string>;
} & {}>> | undefined
What I've Tried
- Manually typing action and passing as generic
const actionData = useActionData<MyActionType>()
- Type casting to manual type
const actionData = useActionData() as MyActionType
- Type casting to unknown first
const actionData = useActionData() as unknown as MyActionType
- My original code used
actionData?.errors?.[input]?._errors[0]
in thehasErrors()
function, but it threw acannot index errors with type string
.
Removing zod and returning a static object of the same format does get rid of the errors, but obviously that is less than ideal. I'm sure someone has run into this since Remix v1.6.5
released. Would love to figure out how to properly use the inferred types and pass back the zod
errors directly.