Skip to content

Fix incorrect type inference: ensure ZodString instead of generic ZodType<string> on convexToZod function types #692

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions packages/convex-helpers/server/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1314,14 +1314,65 @@ export function zBrand<
/** Simple type conversion from a Convex validator to a Zod validator. */
export type ConvexToZod<V extends GenericValidator> = z.ZodType<Infer<V>>;

/** Better type conversion from a Convex validator to a Zod validator where the output is not a generetic ZodType but it's more specific.
*
* ES: z.ZodString instead of z.ZodType<string, z.ZodTypeDef, string>
* so you can use methods of z.ZodString like .min() or .email()
*/

type ZodFromValidatorBase<V extends GenericValidator> =
V extends VId<GenericId<infer TableName extends string>>
? Zid<TableName>
: V extends VString<any, any>
? z.ZodString
Comment on lines +1326 to +1327
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we lose branded types here? e.g. the brandedStringhelper results in aVString<string & {_brand: "foo" },type - can we pass that through to a more specificZodString` type?

: V extends VFloat64<any, any>
? z.ZodNumber
: V extends VInt64<any, any>
? z.ZodBigInt
: V extends VBoolean<any, any>
? z.ZodBoolean
: V extends VNull<any, any>
? z.ZodNull
: V extends VLiteral<any, any>
? z.ZodLiteral<V["value"]>
Comment on lines +1336 to +1337
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be VLiteral<infer T, any> instead of using V["value"]?

: V extends VObject<any, any, any, any>
? z.ZodObject<{
[K in keyof V["fields"]]: ZodValidatorFromConvex<
Comment on lines +1338 to +1340
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here for extracting the type instead of using V["fields"] - though maybe your version avoids some inference challenges I'm unaware of? wdyt?

V["fields"][K]
>;
}>
: V extends VRecord<any, infer Key, any, any, any>
? Key extends VId<GenericId<infer TableName>>
? z.ZodRecord<
Zid<TableName>,
ZodValidatorFromConvex<V["value"]>
>
: z.ZodRecord<
z.ZodString,
ZodValidatorFromConvex<V["value"]>
>
: V extends VArray<any, any>
? z.ZodArray<ZodValidatorFromConvex<V["element"]>>
: V extends VUnion<any, any, any, any>
? z.ZodUnion<
[ZodValidatorFromConvex<V["members"][number]>]
>
: z.ZodTypeAny; // fallback for unknown validators

/** Main type with optional handling. */
export type ZodValidatorFromConvex<V extends GenericValidator> =
V extends Validator<any, "optional", any>
? z.ZodOptional<ZodFromValidatorBase<V>>
: ZodFromValidatorBase<V>;

/**
* Turn a Convex validator into a Zod validator.
* @param convexValidator Convex validator can be any validator from "convex/values" e.g. `v.string()`
* @returns Zod validator (e.g. `z.string()`) with inferred type matching the Convex validator
*/
export function convexToZod<V extends GenericValidator>(
convexValidator: V,
): z.ZodType<Infer<V>> {
): ZodValidatorFromConvex<V> {
const isOptional = (convexValidator as any).isOptional === "optional";

let zodValidator: z.ZodTypeAny;
Expand Down Expand Up @@ -1393,7 +1444,9 @@ export function convexToZod<V extends GenericValidator>(
throw new Error(`Unknown convex validator type: ${convexValidator.kind}`);
}

return isOptional ? z.optional(zodValidator) : zodValidator;
return isOptional
? (z.optional(zodValidator) as ZodValidatorFromConvex<V>)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are optional types going through? I fear above that we're losing that.

: (zodValidator as ZodValidatorFromConvex<V>);
}

/**
Expand All @@ -1408,5 +1461,5 @@ export function convexToZodFields<C extends PropertyValidators>(
) {
return Object.fromEntries(
Object.entries(convexValidators).map(([k, v]) => [k, convexToZod(v)]),
) as { [k in keyof C]: z.ZodType<Infer<C[k]>> };
) as { [k in keyof C]: ZodValidatorFromConvex<C[k]> };
}
Loading