zod
zod copied to clipboard
Zod Preprocess returning unknown type
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>
This depends on what type util you're using to infer the type? Are you using infer, input or output?
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;
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;
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.
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
I'm having the same issue as @spoptchev with zodResolver from react-hook-form. I am also on zod version 3.24.3.
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>;
@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>
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>
}
}
+1
I had this problem with react hook form and zodResolver
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>,
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
Same unknown type issue with react-hook-form
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... 😕
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`