;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.
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
.
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
const actionData = useActionData<MyActionType>()
const actionData = useActionData() as MyActionType
const actionData = useActionData() as unknown as MyActionType
actionData?.errors?.[input]?._errors[0]
in the hasErrors()
function, but it threw a cannot 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.
Just focusing on the error message:
Property '_errors' does not exist on ...[long type mostly of zod errors]
The issue seems to be that you are calling hasErrors
with something that is not in the object returned by the format
method. From testing out that method on my own, it seems like only fields that have errors are returned in the result. Indexing without ?.
will give you the error you are seeing because you're then trying to index [0]
on the undefined
value that was returned.
My original code used
actionData?.errors?.[input]?._errors[0]
in the hasErrors() function, but it threw acannot index errors with type string
.
It seems like you already addressed this problem for yourself by casting input
to keyof typeof actionData.errors
. Doing the same in the code you wrote with ?._errors[0]
should work if I'm not mistaken.
In general, I would suggest avoiding typecasting if you can, however. Is it possible to update hasErrors
input type from string
to keyof typeof actionData.errors
?
To summarize, does this work?
function hasErrors(input: keyof typeof actionData.errors) {
return {
flag: Boolean(actionData?.errors?.[input]),
message: actionData?.errors?.[input]?._errors[0],
}
}