Skip to content

Type inference not working for createServerValidate with Standard Schema (Zod) #1856

@nicksrandall

Description

@nicksrandall

Description

When using createServerValidate from @tanstack/react-form-start with Zod schemas (which implement Standard Schema v1), the return type is Promise<any> instead of inferring the output type from the schema.

Since TanStack Form v1.0+ supports Standard Schema natively, and Zod v4+ properly implements the Standard Schema spec with type information available via StandardSchemaV1.InferOutput, I would expect createServerValidate to infer the validated data type automatically.

Environment

  • @tanstack/react-form-start: 1.25.0
  • @tanstack/form-core: 1.25.0
  • zod: 4.1.12
  • TypeScript: (affects all versions)

Current Behavior

import { createServerValidate, formOptions } from '@tanstack/react-form-start';
import { z } from 'zod';

const emailFormOpts = formOptions({
  defaultValues: {
    email: '',
  },
});

const loginSchema = z.object({
  email: z.string().email(),
  redirect: z.string().default('/'),
});

const serverValidate = createServerValidate({
  ...emailFormOpts,
  onServerValidate: loginSchema,
});

// Type is inferred as Promise<any> instead of Promise<{ email: string; redirect: string }>
const validatedData = await serverValidate(formData);
//    ^? any

Expected Behavior

The return type should be inferred from the Zod schema:

const validatedData = await serverValidate(formData);
//    ^? { email: string; redirect: string }

Root Cause Analysis

After investigating the source code:

1. Type Definition Returns Promise<any>

From createServerValidate.d.ts:

export declare const createServerValidate: <TFormData, ...>(
  defaultOpts: CreateServerValidateOptions<...>
) => (formData: FormData, info?: Parameters<typeof decode>[1]) => Promise<any>;

2. Type Information IS Available

Zod v4's Standard Schema implementation provides type information:

export interface StandardSchemaV1<Input = unknown, Output = Input> {
  readonly "~standard": StandardSchemaV1.Props<Input, Output>;
}

export declare namespace StandardSchemaV1 {
  export type InferOutput<Schema extends StandardSchemaV1> =
    NonNullable<Schema["~standard"]["types"]>["output"];
}

3. TanStack Form Only Extracts Error Types

From @tanstack/form-core/src/FormApi.ts:

export type UnwrapFormAsyncValidateOrFn<
  TValidateOrFn extends undefined | FormAsyncValidateOrFn<any>,
> = [TValidateOrFn] extends [StandardSchemaV1<any, any>]
    ? Record<string, StandardSchemaV1Issue[]>  // Only extracts error type!
    : undefined

The library only extracts validation errors from Standard Schema validators, not the output data type.

Proposed Solution

Create a utility type to extract the output type from Standard Schema validators:

type InferValidatorOutput<TValidator> =
  TValidator extends StandardSchemaV1<any, infer Output>
    ? Output
    : TValidator extends FormValidateAsyncFn<infer TFormData>
      ? TFormData
      : unknown;

Then update createServerValidate's return type to use Promise<InferValidatorOutput<TOnServer>> instead of Promise<any>.

Current Workaround

Users must manually assert the type:

import type { StandardSchemaV1 } from 'zod';

const validatedData = await serverValidate(formData) as StandardSchemaV1.InferOutput<typeof loginSchema>;

// Or using z.infer:
const validatedData = await serverValidate(formData) as z.infer<typeof loginSchema>;

Impact

This affects all users of createServerValidate with Standard Schema validators (Zod, Valibot, ArkType, etc.). Type safety is lost at the boundary between form validation and business logic, requiring manual type assertions throughout the codebase.

Additional Context

This appears to be an oversight rather than intentional design, as:

  1. TanStack Form explicitly supports Standard Schema
  2. Standard Schema provides full type information via ~standard.types
  3. The validated data is returned at runtime with full type information
  4. Only the TypeScript type definition is missing the connection

Would the maintainers be open to a PR implementing this type inference? I'm happy to contribute if this aligns with the project's direction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions