zod icon indicating copy to clipboard operation
zod copied to clipboard

Zod Preprocess returning unknown type

Open enchorb opened this issue 1 year ago • 10 comments

The type returned from z.preprocess is giving out type unknown

export const string_validation = z.preprocess(
  (value) => {
    if (typeof value !== 'string') return value;
    return value.trim().replace(/\s\s+/g, ' ');
  },
  z.string().min(1)
);

export const boolean_validation = z.preprocess((bool) => {
  if (typeof bool === 'boolean') return bool;
  return bool === 'true';
}, z.boolean());

export const number_validation = z.preprocess(
  (num) => (!num && num !== 0 ? num : Number(num)),
  z.number()
);

It's annoying but don't use preprocess in too many places so for now fixing by setting the type explicitly

// Before
export const string_validation: .ZodEffects<z.ZodString, string, unknown>
export const boolean_validation: z.ZodEffects<z.ZodBoolean, boolean, unknown>
export const number_validation: z.ZodEffects<z.ZodNumber, number, unknown>

// After
export const string_validation: .ZodEffects<z.ZodString, string, string>
export const boolean_validation: z.ZodEffects<z.ZodBoolean, boolean, boolean>
export const number_validation: z.ZodEffects<z.ZodNumber, number, number>

enchorb avatar May 30 '24 11:05 enchorb

This depends on what type util you're using to infer the type? Are you using infer, input or output?

samchungy avatar Jun 09 '24 14:06 samchungy

I use infer for my types and that works fine, types are as expected. This fails after safeParse / parse where the output for all preprocessed types is unknown

const product_validation = z.object({
   name: string_validation
   description: z.string(),
   hidden: boolean_validation.default(false),
   popular: z.boolean()
}).partial();

// Types Are As Expected
export type Product = z.infer<typeof product_validation>; 

// Types Are Unknown For All Preprocessed Types
const parsed = product_validation.safeParse({ name: 'Test Product', description: 'This is a product', hidden: false, popular: true )}.data; 

enchorb avatar Oct 18 '24 11:10 enchorb

Looks fine to me in this playground

image

samchungy avatar Oct 19 '24 02:10 samchungy

I think I have a similar issue. For me it appears when using z.ZodType.

See this playground.

import {z} from 'zod';

const preprocessSchema = z.preprocess((val) => (val === '' ? null : val), z.coerce.date());
const noPreprocessSchema = z.coerce.date();

function doSafeParse<T>(schema: z.ZodType<T>) {
  return schema.safeParse({})
}

// unknown -- broken?
const parsed = doSafeParse(preprocessSchema).data;
// Date | undefined
const parsed2 = preprocessSchema.safeParse({}).data;
// Date | undefined
const parsed3 = doSafeParse(noPreprocessSchema).data;
// Date | undefined
const parsed4 = noPreprocessSchema.safeParse({}).data;

andrew-pledge-io avatar Oct 29 '24 15:10 andrew-pledge-io

I think I have a similar issue. For me it appears when using z.ZodType.

See this playground.

import {z} from 'zod';

const preprocessSchema = z.preprocess((val) => (val === '' ? null : val), z.coerce.date());
const noPreprocessSchema = z.coerce.date();

function doSafeParse<T>(schema: z.ZodType<T>) {
  return schema.safeParse({})
}

// unknown -- broken?
const parsed = doSafeParse(preprocessSchema).data;
// Date | undefined
const parsed2 = preprocessSchema.safeParse({}).data;
// Date | undefined
const parsed3 = doSafeParse(noPreprocessSchema).data;
// Date | undefined
const parsed4 = noPreprocessSchema.safeParse({}).data;

You aren't declaring your function types correctly. Please read this.

samchungy avatar Oct 30 '24 06:10 samchungy

I am facing the same issue when using zodResolver from react-hook-form:

const keywordSchema = z.preprocess(
  (arg): string[] => {
    if (typeof arg === "string") {
      return arg.trim() === "" ? [] : arg.split(/\r?\n/);
    }

    return [];
  },
  z.array(z.string()),
)

export const dummySchema = z.object({
  keywords: keywordSchema,
});

resolver: zodResolver(dummySchema)

// -> has no properties in common with type { keywords?: unknown; }

zod Version: 3.24.3

spoptchev avatar Apr 24 '25 12:04 spoptchev

I'm having the same issue as @spoptchev with zodResolver from react-hook-form. I am also on zod version 3.24.3.

MichaelCharles avatar Apr 25 '25 07:04 MichaelCharles

Since is not fixed you can force the return type like this:

export const trimed = z.preprocess(
  (value) => {
    return value.trim();
  },
  z.string().min(1)
) as ZodEffects<ZodString, string, string>;

comxd avatar Apr 25 '25 08:04 comxd

@spoptchev Same here, I also have problem with the handleSubmit:

Argument of type '(data: ZodFormData) => void' is not assignable to parameter of type 'SubmitHandler<TFieldValues>'.
  Types of parameters 'data' and 'data' are incompatible.
    Type 'TFieldValues' is not assignable to type '{ phoneNumber: string; }'.
      Property 'phoneNumber' is missing in type 'FieldValues' but required in type '{ phoneNumber: string; }'.ts(2345)

@comxd solved my problem, but I needed to add unknown before ZodEffects:

as unknown as ZodEffects<ZodString, string, string>

vagnerlandio avatar May 19 '25 12:05 vagnerlandio

I managed to fix this by overriding the type definition:

import "zod";
import type {
  RawCreateParams,
  RefinementCtx,
  ZodEffects,
  ZodTypeAny,
} from "zod";

declare module "zod" {
  namespace z {
    function preprocess<Schema extends ZodTypeAny>(
      preprocess: (arg: unknown, ctx: RefinementCtx) => unknown,
      schema: Schema,
      params?: RawCreateParams
    ): ZodEffects<Schema, Schema["_output"], Schema["_input"]>; // It was ZodEffects<Schema, Schema["_output"], unknown>
  }
}

ayloncarrijo avatar May 23 '25 00:05 ayloncarrijo

+1

I had this problem with react hook form and zodResolver

paschaldev avatar Jun 03 '25 16:06 paschaldev

I think maybe some of the internal types have changed.

My situation

export const coerceToISOString = (value: unknown): string => {
  if (value instanceof Date) {
    return value.toISOString()
  } else {
    return String(value)
  }
}
/// ...rest of zod object
endEffectiveDate: z.preprocess(
      coerceToISOString,
      z.iso.datetime('End Date is Required'),
    ) as z.ZodPipe<z.ZodTransform<string | Date, string>, z.ZodISODateTime>,

uap-dev avatar Jul 17 '25 00:07 uap-dev

This happens because TypeScript can't infer the correct output type from a generic like z.ZodType<T> when the schema involves effects like z.preprocess. The generic parameter only captures the output type, not the full schema type, so you lose the transformation information and get unknown for .data in your function.

To preserve type inference, you should type your function like this:

import { z, ZodTypeAny } from "zod";

function doSafeParse<T extends ZodTypeAny>(schema: T) {
  return schema.safeParse({}) as z.SafeParseReturnType<unknown, z.output<T>>;
}

// Now this works as expected:
const parsed = doSafeParse(preprocessSchema).data; // Date | undefined

Or, even better, use the pattern from the docs and issues:

function parseData<T extends z.ZodTypeAny>(schema: T, data: unknown): z.output<T> {
  return schema.parse(data);
}

See the official docs on writing generic functions and this GitHub issue for more details and examples.

Let me know if this solves your problem—if so, feel free to close the issue!

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

dosubot[bot] avatar Jul 21 '25 23:07 dosubot[bot]

Same unknown type issue with react-hook-form

masoudmoghaddari avatar Oct 16 '25 11:10 masoudmoghaddari

I managed to fix this by overriding the type definition:

-- @ayloncarrijo

This fix only seem to work for v3. Anyone been able to fix this for v4?

Really wish this could be fixed somehow as it's rather frustrating not being able to use z.preprocess with react-hook-form and similar libraries... 😕

Svish avatar Oct 29 '25 13:10 Svish

I've seen someone argue that with z.preprocess you don't know what the input type is because it could be anything, but that doesn't really make sense because then the input type of all schemas really should be unknown. Even if you have z.string(), you don't know what will be passed to it, that's the point of the validation/parsing to begin with. The input type shouldn't be "what do we know it is", but "what do we expect it to be".

If I have z.preprocess((val) => ..., z.string()), it should be irrelevant what the preprocessing does or doesn't do, the expected input type is obviously string.

A slightly sketchy "workaround" seem to be to declare the parameter type on the preprocess function, e.g. like this:

const accountNumberSchema = z.preprocess(
  (value: string | null | undefined) => typeof value === 'string'
    ? value.replace(/\D+/g, '')
    : value,
  z.stringFormat('accountNumber', isAccountNumber)
)

type Input = z.input<typeof accountNumberSchema>;
//   ^^ string | null | undefined

Sketchy, because we of course have no idea if what will pass through the preprocess function actually is a string or not, so we need to remember to guard ourselves against that, even though typescript will not remind us.

The absolute best solution here would be if z.preprocess just "copied" the input type of whatever schema was passed to it as the second parameter.

Basically, the following two schemas really should have the exact same inferred input and output types, while the preprocessor function itself should still be unknown:

const test1 = z.string();
const test2 = z.preprocess((value) => value, z.string());
//                         ^ fn should always be `(value: unknown): unknown`

Svish avatar Oct 29 '25 13:10 Svish