Skip to content

Commit 369e7d6

Browse files
feat: expose stronger typed SubmitFunction (#9201)
partially implements #7161 --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
1 parent ae6ddad commit 369e7d6

5 files changed

Lines changed: 115 additions & 15 deletions

File tree

.changeset/stupid-trains-push.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: expose stronger typed `SubmitFunction` through `./$types`

packages/kit/src/core/sync/write_types/index.js

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -390,16 +390,23 @@ function process_node(node, outdir, is_page, proxies, all_pages_have_load = true
390390

391391
if (is_page) {
392392
let type = 'unknown';
393-
if (proxy) {
394-
if (proxy.exports.includes('actions')) {
395-
// If the file wasn't tweaked, we can use the return type of the original file.
396-
// The advantage is that type updates are reflected without saving.
397-
const from = proxy.modified
398-
? `./proxy${replace_ext_with_js(basename)}`
399-
: path_to_original(outdir, node.server);
400-
401-
type = `Expand<Kit.AwaitedActions<typeof import('${from}').actions>> | null`;
402-
}
393+
if (proxy && proxy.exports.includes('actions')) {
394+
// If the file wasn't tweaked, we can use the return type of the original file.
395+
// The advantage is that type updates are reflected without saving.
396+
const from = proxy.modified
397+
? `./proxy${replace_ext_with_js(basename)}`
398+
: path_to_original(outdir, node.server);
399+
400+
exports.push(
401+
`type ExcludeActionFailure<T> = T extends Kit.ActionFailure<any> ? never : T extends void ? never : T;`,
402+
`type ActionsSuccess<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: ExcludeActionFailure<Awaited<ReturnType<T[Key]>>>; }[keyof T];`,
403+
`type ExtractActionFailure<T> = T extends Kit.ActionFailure<infer X> ? X extends void ? never : X : never;`,
404+
`type ActionsFailure<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: Exclude<ExtractActionFailure<Awaited<ReturnType<T[Key]>>>, void>; }[keyof T];`,
405+
`type ActionsExport = typeof import('${from}').actions`,
406+
`export type SubmitFunction = Kit.SubmitFunction<Expand<ActionsSuccess<ActionsExport>>, Expand<ActionsFailure<ActionsExport>>>`
407+
);
408+
409+
type = `Expand<Kit.AwaitedActions<ActionsExport>> | null`;
403410
}
404411
exports.push(`export type ActionData = ${type};`);
405412
}

packages/kit/src/core/sync/write_types/index.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ test('Creates correct $types', async () => {
3333
// To safe us from creating a real SvelteKit project for each of the tests,
3434
// we first run the type generation directly for each test case, and then
3535
// call `tsc` to check that the generated types are valid.
36+
await run_test('actions');
3637
await run_test('simple-page-shared-only');
3738
await run_test('simple-page-server-only');
3839
await run_test('simple-page-server-and-shared');
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { fail } from '../../../../../../types/internal.js';
2+
3+
let condition = false;
4+
5+
export const actions = {
6+
default: () => {
7+
if (condition) {
8+
return fail(400, {
9+
fail: 'oops'
10+
});
11+
}
12+
13+
return {
14+
success: true
15+
};
16+
},
17+
successWithPayload: () => {
18+
return {
19+
id: 42,
20+
username: 'John Doe',
21+
profession: 'Svelte specialist'
22+
};
23+
},
24+
successWithoutPayload: () => {},
25+
failWithPayload: () => {
26+
return fail(400, {
27+
reason: {
28+
error: {
29+
code: /** @type {const} */ ('VALIDATION_FAILED')
30+
}
31+
}
32+
});
33+
},
34+
failWithoutPayload: () => {
35+
return fail(400);
36+
}
37+
};
38+
39+
/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/actions/$types').SubmitFunction} */
40+
const submit = () => {
41+
return ({ result }) => {
42+
if (result.type === 'success') {
43+
// @ts-expect-error does only exist on `failure` result
44+
result.data?.fail;
45+
// @ts-expect-error unknown property
46+
result.data?.something;
47+
48+
if (result.data && 'success' in result.data) {
49+
result.data.success === true;
50+
// @ts-expect-error should be of type `boolean`
51+
result.data.success === 'success';
52+
// @ts-expect-error does not exist in this branch
53+
result.data.id;
54+
}
55+
56+
if (result.data && 'id' in result.data) {
57+
result.data.id === 42;
58+
// @ts-expect-error should be of type `number`
59+
result.data.id === 'John';
60+
// @ts-expect-error does not exist in this branch
61+
result.data.success;
62+
}
63+
}
64+
65+
if (result.type === 'failure') {
66+
result.data;
67+
// @ts-expect-error does only exist on `success` result
68+
result.data.success;
69+
// @ts-expect-error unknown property
70+
result.data.unknown;
71+
72+
if (result.data && 'fail' in result.data) {
73+
result.data.fail === '';
74+
// @ts-expect-error does not exist in this branch
75+
result.data.reason;
76+
}
77+
78+
if (result.data && 'reason' in result.data) {
79+
result.data.reason.error.code === 'VALIDATION_FAILED';
80+
// @ts-expect-error should be a const
81+
result.data.reason.error.code === '';
82+
// @ts-expect-error does not exist in this branch
83+
result.data.fail;
84+
}
85+
}
86+
};
87+
};

packages/kit/types/index.d.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,10 +1167,10 @@ export type Actions<
11671167
*/
11681168
export type ActionResult<
11691169
Success extends Record<string, unknown> | undefined = Record<string, any>,
1170-
Invalid extends Record<string, unknown> | undefined = Record<string, any>
1170+
Failure extends Record<string, unknown> | undefined = Record<string, any>
11711171
> =
11721172
| { type: 'success'; status: number; data?: Success }
1173-
| { type: 'failure'; status: number; data?: Invalid }
1173+
| { type: 'failure'; status: number; data?: Failure }
11741174
| { type: 'redirect'; status: number; location: string }
11751175
| { type: 'error'; status?: number; error: any };
11761176

@@ -1239,7 +1239,7 @@ export function text(body: string, init?: ResponseInit): Response;
12391239
* @param status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
12401240
* @param data Data associated with the failure (e.g. validation errors)
12411241
*/
1242-
export function fail<T extends Record<string, unknown> | undefined>(
1242+
export function fail<T extends Record<string, unknown> | undefined = undefined>(
12431243
status: number,
12441244
data?: T
12451245
): ActionFailure<T>;
@@ -1257,7 +1257,7 @@ export interface ActionFailure<T extends Record<string, unknown> | undefined = u
12571257

12581258
export interface SubmitFunction<
12591259
Success extends Record<string, unknown> | undefined = Record<string, any>,
1260-
Invalid extends Record<string, unknown> | undefined = Record<string, any>
1260+
Failure extends Record<string, unknown> | undefined = Record<string, any>
12611261
> {
12621262
(input: {
12631263
action: URL;
@@ -1271,7 +1271,7 @@ export interface SubmitFunction<
12711271
| ((opts: {
12721272
form: HTMLFormElement;
12731273
action: URL;
1274-
result: ActionResult<Success, Invalid>;
1274+
result: ActionResult<Success, Failure>;
12751275
/**
12761276
* Call this to get the default behavior of a form submission response.
12771277
* @param options Set `reset: false` if you don't want the `<form>` values to be reset after a successful submission.

0 commit comments

Comments
 (0)