kit
kit copied to clipboard
Types for enhance function callback
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.

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.

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
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>
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
>;
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.
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);
}
};
};
@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.
Yes @hyunbinseo you are right, I think we can close this now :)
partially implements https://github.com/sveltejs/kit/issues/7161
Note that this issue is not completely resolved.
Maybe it should stay open in the whenever (non-urgent) milestone?