diff --git a/.changeset/poor-loops-boil.md b/.changeset/poor-loops-boil.md new file mode 100644 index 000000000000..49a8ab8b1cd9 --- /dev/null +++ b/.changeset/poor-loops-boil.md @@ -0,0 +1,24 @@ +--- +'astro': minor +--- + +Adds the `ActionInputSchema` utility type to automatically infer the TypeScript type of an action's input based on its Zod schema + +For example, this type can be used to retrieve the input type of a form action: + +```ts +import { type ActionInputSchema, defineAction } from 'astro:actions'; +import { z } from 'astro/zod'; + +const action = defineAction({ + accept: 'form', + input: z.object({ name: z.string() }), + handler: ({ name }) => ({ message: `Welcome, ${name}!` }), +}); + +type Schema = ActionInputSchema; +// typeof z.object({ name: z.string() }) + +type Input = z.input; +// { name: string } +``` diff --git a/packages/astro/src/actions/runtime/server.ts b/packages/astro/src/actions/runtime/server.ts index 9fc12183fc76..9c11cf4ab79c 100644 --- a/packages/astro/src/actions/runtime/server.ts +++ b/packages/astro/src/actions/runtime/server.ts @@ -38,6 +38,19 @@ export type ActionHandler = TInputSchema extends z.ZodTyp export type ActionReturnType> = Awaited>; +const inferSymbol = Symbol('#infer'); + +/** + * Infers the type of an action's input based on its Zod schema + * + * @see https://docs.astro.build/en/reference/modules/astro-actions/#actioninputschema + */ +export type ActionInputSchema> = T extends { + [inferSymbol]: any; +} + ? T[typeof inferSymbol] + : never; + export type ActionClient< TOutput, TAccept extends ActionAccept | undefined, @@ -57,6 +70,7 @@ export type ActionClient< orThrow: ( input: TAccept extends 'form' ? FormData : z.input, ) => Promise>; + [inferSymbol]: TInputSchema; } : ((input?: any) => Promise>>) & { orThrow: (input?: any) => Promise>; diff --git a/packages/astro/test/types/action-input-schema.ts b/packages/astro/test/types/action-input-schema.ts new file mode 100644 index 000000000000..d88e5bb30dee --- /dev/null +++ b/packages/astro/test/types/action-input-schema.ts @@ -0,0 +1,51 @@ +import { describe, it } from 'node:test'; +import { expectTypeOf } from 'expect-type'; +import { type ActionInputSchema, defineAction } from '../../dist/actions/runtime/server.js'; +import { z } from '../../dist/zod.js'; + +describe('ActionInputSchema', () => { + const acceptVariants = ['form', 'json', undefined] as const; + + for (const accept of acceptVariants) { + describe(`accept = ${typeof accept === 'string' ? `'${accept}'` : accept}`, () => { + it('Infers action input schema', async () => { + const inputSchema = z.object({ + name: z.string(), + age: z.number(), + }); + + const _action = defineAction({ + accept, + input: inputSchema, + handler: () => undefined, + }); + + expectTypeOf>().toEqualTypeOf(); + }); + + it('Infers action input value', async () => { + const schema = z.object({ + name: z.string(), + age: z.number(), + }); + const _action = defineAction({ + accept, + input: schema, + handler: () => undefined, + }); + expectTypeOf>>().toEqualTypeOf<{ + name: string; + age: number; + }>(); + }); + + it('Infers action input schema when input is omitted', async () => { + const _action = defineAction({ + accept, + handler: () => undefined, + }); + expectTypeOf>().toBeNever; + }); + }); + } +});