Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Types for enhance function callback #7161

Closed
ftognetto opened this issue Oct 6, 2022 · 7 comments
Closed

Types for enhance function callback #7161

ftognetto opened this issue Oct 6, 2022 · 7 comments

Comments

@ftognetto
Copy link

Describe the problem

I would like to have the result object parameter of enhance function to be typed.

Example:

+page.server.ts

import { invalid, type Actions } from '@sveltejs/kit';

export const actions: Actions = {
	default: async (event) => {
                // get data from form
		const data = await event.request.formData();
		const name = data.get('name');
		const surname = data.get('surname');

		// validation
		const errors = validate({name, surname});
		if (errors) {
			return invalid(400, {
				name,
				surname,
				errors
			});
		}
		// call external be or db
		const result = await fetch('<url>', { body: JSON.stringify({name, surname}) });
		if (!result.ok) {
			return invalid(500, {
				name, 
				surname,
				beError: await result.text()
			})
		}
		return {
			name,
			surname,
			result: await result.json()
		}
	}
};

+page.svelte

<script lang="ts">
	import { applyAction, enhance } from '$app/forms';
	import type { ActionData } from './$types';
	export let form: ActionData;
</script>

<form
	method="POST"
	use:enhance={(event) => {
		return async ({ result }) => {
			if (result.type === 'success') {
				alert('hello ' + result.data?.name);
			}
			applyAction(result);
		};
	}}
>
	<input type="text" name="name" placeholder="Name" value={form?.name || ''} />
	<input type="text" name="surname" placeholder="Surname" value={form?.surname || ''} />

	<button type="submit" class="btn btn-primary ">Salva</button>
</form>

So in +page.svelte i have the correct types on form object so that in input value <input type="text" name="name" placeholder="Name" value={form?.name || ''} /> the editor suggest me that form object has a name property.
Schermata 2022-10-06 alle 07 00 22

Insted in enhance callback the result.data is a Record<string, any> | undefined and I don't know what kind of data the +page.server.ts sent.
Schermata 2022-10-06 alle 07 04 08

Describe the proposed solution

I'm trying to find a solution but for now the only workaround that is working for me is to not use enhance but instead use a custom listener like the docs says (https://kit.svelte.dev/docs/form-actions#progressive-enhancement-custom-event-listener) but I'm afraid that without the use of the enhance function I lose the ability to make the form work in the absence of javascript.

Alternatives considered

No response

Importance

would make my life easier

Additional Information

No response

@david-plugge
Copy link
Contributor

david-plugge commented Oct 6, 2022

I think it would be nice if the success and invalid data are separated and exported from ./$types.
For the moment you can take a look at this:

// src/lib/form.ts
import { type SubmitFunction, enhance } from '$app/forms';

type SuccessData<T> = T extends Record<string, unknown> ? T extends { invalid: boolean } ? never : T : never;
type InvalidData<T> = T extends Record<string, unknown> ? T extends { invalid: boolean} ? T : never : never;
export type TypedSubmitFunction<T> = SubmitFunction<SuccessData<T>, InvalidData<T>>

If you add invalid: true to the invalid data you will get correct types

// src/routes/+page.server.ts
import { invalid } from '@sveltejs/kit';
import type { Actions } from './$types';

export const actions: Actions = {
	async default() {
		if (somecheck()) {
			return invalid(400, {
				someError: 'There was an error!',
				invalid: true
			});
		}

		return {
			success: true
		};
	}
};
<!--  src/routes/+page.svelte -->
<script lang="ts">
	import { enhance } from '$app/forms';
	import type { TypedSubmitFunction } from '$lib/form';
	import type { ActionData } from './$types';

	export let form: ActionData;

	const handler: TypedSubmitFunction<ActionData> = () => {
		return ({ update,result }) => {
            if (result.type === 'invalid') {
                result.data
            }
            if (result.type === 'success') {
                result.data
            }
			update();
		};
	};
</script>

<form method="post" use:enhance={handler}>
	<button type="submit">Submit</button>
</form>

@ftognetto
Copy link
Author

ftognetto commented Oct 6, 2022

hi @david-plugge thank you! I was working on a similar solution, declaring a new SubmitFunction passing the correct types.
But I think it would be nice that enhance function infer types returned from the +page.server.ts action.

So for example if I write an action that

export const actions: Actions = {
	default: async (event): Promise<FormResult<User>> => {
            ....
        }
}

with, in my case, FormResult from

export type RecordOrUndefined = Record<string, unknown> | undefined;
export type FormErrors = { [key: string]: string | undefined; generic?: string | undefined };
export type FormValues = { [key: string]: string | undefined };

export type FormSuccessResult<T extends RecordOrUndefined> = {
	values: FormValues;
	output?: T;
};
export type FormInvalidResult = {
	values: FormValues;
	errors: FormErrors;
};
export type FormResult<T extends RecordOrUndefined = undefined> = FormSuccessResult<T> | ValidationError<FormInvalidResult>;

Then the submit function would infer Success and Invalid types, passing type User to result.data.output in case of success.

The type of result in enhance function should be

export type FormActionResult<T extends RecordOrUndefined> = ActionResult<
	FormSuccessResult<T>,
	FormInvalidResult
>;

@david-plugge
Copy link
Contributor

david-plugge commented Oct 6, 2022

But I think it would be nice that enhance function infer types returned from the +page.server.ts action.

If you mean automagically inferring the correct types by the current path + action name, then probably no, that is way to complicated.
Passing ActionData to SubmitFunction should be possible to implement and is actually a pretty nice idea.

@Rich-Harris Rich-Harris added this to the whenever milestone Nov 16, 2022
dummdidumm added a commit that referenced this issue Mar 30, 2023
partially implements #7161

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
@ZerdoX-x
Copy link

ZerdoX-x commented Nov 22, 2023

Could someone clarify how can I type enhance function now? I saw commit on March 30 but I am not sure how to get benefits from it and how to type it correctly now.

const enhanceTagSuggestionForm =
	() => {
		return (event) => {
			const result = event.result;
			if (result.type === "failure") {
				alert("something went wrong");
			} else if (result.type === "success") {
				if (typeof result?.data?.data?.competency_name !== "string")
					return alert("something went wrong");
				pushCompetencyTag(result.data.data.competency_name);
			}
		};
	};
  112:10  error  Unsafe assignment of an `any` value                                     @typescript-eslint/no-unsafe-assignment
  112:19  error  Unsafe member access .result on an `any` value                          @typescript-eslint/no-unsafe-member-access
  113:8   error  Unsafe member access .type on an `any` value                            @typescript-eslint/no-unsafe-member-access
  115:15  error  Unsafe member access .type on an `any` value                            @typescript-eslint/no-unsafe-member-access
  117:16  error  Unsafe member access .data on an `any` value                            @typescript-eslint/no-unsafe-member-access
  120:23  error  Unsafe argument of type `any` assigned to a parameter of type `string`  @typescript-eslint/no-unsafe-argument
  120:23  error  Unsafe member access .data on an `any` value                            @typescript-eslint/no-unsafe-member-access

UPD: This is what I am currently using

import type { ActionResult } from "@sveltejs/kit";

const enhanceTagSuggestionForm = () => {
	return ({ result }: { result: ActionResult }) => {
		if (result.type === "failure") {
			alert("something went wrong");
		} else if (result.type === "success") {
			// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
			if (typeof result?.data?.data?.competency_name !== "string")
				return alert("something went wrong");
			// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
			pushCompetencyTag(result.data.data.competency_name as string);
		}
	};
};

@hyunbinseo
Copy link
Contributor

@ZerdoX-x As of now, the generated SubmitFunction type has to be manually imported and applied.

<script lang="ts">
  import type { SubmitFunction } from './$types';

  const submitFunction: SubmitFunction = () => {
    return async ({ result }) => {
      if (result.type === 'success' && result.data) {
        result.data; // typed
      }
      if (result.type === 'failure' && result.data) {
        result.data; // typed
      }
    };
  };
</script>

Hope this was part of the zero-effort type safety.

@ftognetto
Copy link
Author

Yes @hyunbinseo you are right, I think we can close this now :)

@hyunbinseo
Copy link
Contributor

partially implements #7161

Note that this issue is not completely resolved.

Maybe it should stay open in the whenever (non-urgent) milestone?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants