Search code examples
typescriptsveltesveltekitpocketbase

In Sveltekit, can you return 2 fail ActionResults?


I have a form action in my +page.server.ts file and I am returning a fail if it fails the zod schema validation which is working. However, when I returning another fail when PocketBase throws an error of email already in use, I get this type error for every field expect email.

Typescript Warning

Property 'password' does not exist on type '{ password?: string[] | undefined; passwordConfirm?: string[] | undefined; email?: string[] | undefined; name?: string[] | undefined; terms?: string[] | undefined; } | { email: string[]; }'.
  Property 'password' does not exist on type '{ email: string[]; }'

Questions:

  1. Is it possible to send more than 1 fail from a +page.server.ts file?
  2. How can I properly fix these type warnings?
  3. Am I doing this correctly, if not how can I fix it?

+page.svelte

...
    <input id="password"
           name="password"
           type="password"
           placeholder="Password"
           class="input input-bordered w-full" 
    />
    <label class="label" for="password">
        {#if form?.errors?.password}
                       <!--^^^Type Error Here-->
            <span class="label-text-alt text-error">{form?.errors?.password[0]}</span>
                                                               <!--^^^Type Error Here-->
        {/if}
    </label>
...
//Type of errors coming from +page.server.ts into +page.svelte
(property) errors: {
    password?: string[] | undefined;
    passwordConfirm?: string[] | undefined;
    email?: string[] | undefined;
    name?: string[] | undefined;
    terms?: string[] | undefined;
} | {
    email: string[];
} | undefined

+page.server.ts

export const actions: Actions = {
    default: async ({ locals, request }) => {
        const formData = Object.fromEntries(await request.formData()) as {
            email: string;
            password: string;
            passwordConfirm: string;
            name: string;
            terms: string;
        };

        const result = registerSchema.safeParse(formData);

        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        const { password, passwordConfirm, ...rest } = formData;

        if (!result.success) {
            const { fieldErrors: errors } = result.error.flatten();
            const data = {
                data: rest,
                errors
            };
            return fail(400, data);
        }

        try {
            await locals.pb.collection('users').create(formData);
            await locals.pb.collection('users').authWithPassword(formData.email, formData.password);
        } catch (e) {
            if (e instanceof ClientResponseError) {
                const data = {
                    data: rest,
                    errors: {
                          email: ['Email already exists']
                    }
                };
                return fail(400, data);
            }
            throw error(500, 'Something went wrong');
        }
        throw redirect(303, '/');
    }
};

Solution

  • You can only return one fail result.

    If you keep the two failure results, but want to fix the TS issue, you could declare a type and assert that the errors of the email check conform to it. E.g.

    const formData = Object.fromEntries(await request.formData()) as {
        email: string;
        password: string;
    };
    
    const schema = z.object({
        email: z.string().email(),
        password: z.string().min(8),
    });
    
    // This matches errors of Zob validation
    type Errors = Partial<Record<keyof typeof formData, string[]>>;
    
    const result = schema.safeParse(formData);
    if (!result.success) {
        return fail(400, {
            errors: result.error.flatten().fieldErrors,
        });
    }
    
    if (formData.email == '[email protected]') {
        return fail(400, {
            errors: {
                email: ['already exists'],
            } as Errors, // This ensures that the errors type is the same
        });
    }
    
    return { message: 'ok' };
    

    Note that as Errors will not catch extraneous properties. So if you misspell a property you do not get a compiler error.

    The type of errors is derived from all possible returned values of the function, so if you first merge all errors into one object and only return one failure result, the type mismatch should already be taken care of by the merging.

    If there were async validation support in Zod, you could just do the full validation that way. Right now you probably would have to do it manually or use an object merging library.