Search code examples
typescriptremixzod

TypeScript error when using Remix action inferred types


;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 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.


Solution

  • 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 a cannot 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],
      }
    }