typescriptsveltesveltekit

Struggling with successfully typing ActionResults for form actions in SvelteKit 2


I'm trying to return the created object from a POST request to the page. Everything works, but the Svelte Vite dev server (which is running) seems to not be comprehending the types properly, resulting in me having to go through any to get the data I need.

The action has the correct inferred return type when I hover it, but result in the enhancer callback is stuck with the type: ActionResult<Record<string, unknown> | undefined, Record<string, unknown> | undefined>.

Here's the relevant action in +page.server.ts:

export const actions: Actions = {
    newPage: async ({ locals }) => {
        if (locals.user == null) return fail(403);

        const project = await getUserDefaultProject(locals.dbclient, locals.user.id);
        if (project == null) return fail(500);

        const newPage = await createNewPage(locals.dbclient, locals.user, project.id);
        if (newPage) return newPage;

        return fail(400);
    },
};

(newPage has the type <PageMetadata|null>)

and the enhanced form code in +page.svelte:

<form
    action="?/newPage"
    method="POST"
    use:enhance={({ cancel }) => {
        if (!loggedIn.state || offlineMode.state) {
            cancel();
            createNewLocalPage();
            return;
        }
        return async ({ result }) => {
            if (result.status == 200) {
                const newPage = (result as any).data as PageMetadata;
                pages.state = [...pages.state, newPage];
            }
        };
    }}
>
    <button type="submit" title="create a new page" id="create-new-page">
        <Fa icon={faPlus} color="var(--background)" />
        <div>new page</div>
    </button>
</form>

I've been searching for a while for how to properly type this and have come up with nothing, so any help would be appreciated.


Solution

  • Without minimally reproducible code I can't give you a very complete answer, but I believe changing if (result.status == 200) for if (result.type === 'success') to validate your result in the enhancer should do it, your form would look like this:

    <form
        action="?/newPage"
        method="POST"
        use:enhance={({ cancel }) => {
            if (!loggedIn.state || offlineMode.state) {
                cancel();
                createNewLocalPage();
                return;
            }
            return async ({ result }) => {
                if (result.type === 'success'){
                    const newPage = result.data;
                    pages.state = [...pages.state, newPage];
                }
            };
        }}
    >
        <button type="submit" title="create a new page" id="create-new-page">
            <Fa icon={faPlus} color="var(--background)" />
            <div>new page</div>
        </button>
    </form>
    

    Explanation: as per the documentation the reason you may be getting an error such as Property 'data' does not exist on type 'ActionResult<Record<string, unknown> | undefined, Record<string, unknown> | undefined>' may come from validating the result with result.status === 200 because that satisfies all the shapes of the return type, where some don't have the data property. Typescript will only accept looking for result.data if your validation ensures a shape where the property exists. that would be result.type === 'success' || result.type === 'failure'

    ActionResult When calling a form action via fetch, the response will be one of these shapes.

    <form method="post" use:enhance={() => {
      return ({ result }) => {
          // result is of type ActionResult
      };
    }}
    type ActionResult<
      Success extends
          | Record<string, unknown>
          | undefined = Record<string, any>,
      Failure extends
          | Record<string, unknown>
          | undefined = Record<string, any>
    > =
      | { type: 'success'; status: number; data?: Success }
      | { type: 'failure'; status: number; data?: Failure }
      | { type: 'redirect'; status: number; location: string }
      | { type: 'error'; status?: number; error: any };