Skip to content

Commit

Permalink
feat: expose stronger typed SubmitFunction (#9201)
Browse files Browse the repository at this point in the history
partially implements #7161

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
  • Loading branch information
ivanhofer and dummdidumm authored Mar 30, 2023
1 parent ae6ddad commit 369e7d6
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/stupid-trains-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: expose stronger typed `SubmitFunction` through `./$types`
27 changes: 17 additions & 10 deletions packages/kit/src/core/sync/write_types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,16 +390,23 @@ function process_node(node, outdir, is_page, proxies, all_pages_have_load = true

if (is_page) {
let type = 'unknown';
if (proxy) {
if (proxy.exports.includes('actions')) {
// If the file wasn't tweaked, we can use the return type of the original file.
// The advantage is that type updates are reflected without saving.
const from = proxy.modified
? `./proxy${replace_ext_with_js(basename)}`
: path_to_original(outdir, node.server);

type = `Expand<Kit.AwaitedActions<typeof import('${from}').actions>> | null`;
}
if (proxy && proxy.exports.includes('actions')) {
// If the file wasn't tweaked, we can use the return type of the original file.
// The advantage is that type updates are reflected without saving.
const from = proxy.modified
? `./proxy${replace_ext_with_js(basename)}`
: path_to_original(outdir, node.server);

exports.push(
`type ExcludeActionFailure<T> = T extends Kit.ActionFailure<any> ? never : T extends void ? never : T;`,
`type ActionsSuccess<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: ExcludeActionFailure<Awaited<ReturnType<T[Key]>>>; }[keyof T];`,
`type ExtractActionFailure<T> = T extends Kit.ActionFailure<infer X> ? X extends void ? never : X : never;`,
`type ActionsFailure<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: Exclude<ExtractActionFailure<Awaited<ReturnType<T[Key]>>>, void>; }[keyof T];`,
`type ActionsExport = typeof import('${from}').actions`,
`export type SubmitFunction = Kit.SubmitFunction<Expand<ActionsSuccess<ActionsExport>>, Expand<ActionsFailure<ActionsExport>>>`
);

type = `Expand<Kit.AwaitedActions<ActionsExport>> | null`;
}
exports.push(`export type ActionData = ${type};`);
}
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/core/sync/write_types/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ test('Creates correct $types', async () => {
// To safe us from creating a real SvelteKit project for each of the tests,
// we first run the type generation directly for each test case, and then
// call `tsc` to check that the generated types are valid.
await run_test('actions');
await run_test('simple-page-shared-only');
await run_test('simple-page-server-only');
await run_test('simple-page-server-and-shared');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { fail } from '../../../../../../types/internal.js';

let condition = false;

export const actions = {
default: () => {
if (condition) {
return fail(400, {
fail: 'oops'
});
}

return {
success: true
};
},
successWithPayload: () => {
return {
id: 42,
username: 'John Doe',
profession: 'Svelte specialist'
};
},
successWithoutPayload: () => {},
failWithPayload: () => {
return fail(400, {
reason: {
error: {
code: /** @type {const} */ ('VALIDATION_FAILED')
}
}
});
},
failWithoutPayload: () => {
return fail(400);
}
};

/** @type {import('./.svelte-kit/types/src/core/sync/write_types/test/actions/$types').SubmitFunction} */
const submit = () => {
return ({ result }) => {
if (result.type === 'success') {
// @ts-expect-error does only exist on `failure` result
result.data?.fail;
// @ts-expect-error unknown property
result.data?.something;

if (result.data && 'success' in result.data) {
result.data.success === true;
// @ts-expect-error should be of type `boolean`
result.data.success === 'success';
// @ts-expect-error does not exist in this branch
result.data.id;
}

if (result.data && 'id' in result.data) {
result.data.id === 42;
// @ts-expect-error should be of type `number`
result.data.id === 'John';
// @ts-expect-error does not exist in this branch
result.data.success;
}
}

if (result.type === 'failure') {
result.data;
// @ts-expect-error does only exist on `success` result
result.data.success;
// @ts-expect-error unknown property
result.data.unknown;

if (result.data && 'fail' in result.data) {
result.data.fail === '';
// @ts-expect-error does not exist in this branch
result.data.reason;
}

if (result.data && 'reason' in result.data) {
result.data.reason.error.code === 'VALIDATION_FAILED';
// @ts-expect-error should be a const
result.data.reason.error.code === '';
// @ts-expect-error does not exist in this branch
result.data.fail;
}
}
};
};
10 changes: 5 additions & 5 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1167,10 +1167,10 @@ export type Actions<
*/
export type ActionResult<
Success extends Record<string, unknown> | undefined = Record<string, any>,
Invalid 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?: Invalid }
| { type: 'failure'; status: number; data?: Failure }
| { type: 'redirect'; status: number; location: string }
| { type: 'error'; status?: number; error: any };

Expand Down Expand Up @@ -1239,7 +1239,7 @@ export function text(body: string, init?: ResponseInit): Response;
* @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.
* @param data Data associated with the failure (e.g. validation errors)
*/
export function fail<T extends Record<string, unknown> | undefined>(
export function fail<T extends Record<string, unknown> | undefined = undefined>(
status: number,
data?: T
): ActionFailure<T>;
Expand All @@ -1257,7 +1257,7 @@ export interface ActionFailure<T extends Record<string, unknown> | undefined = u

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

0 comments on commit 369e7d6

Please sign in to comment.