Simple `ZodObject` inference from existing Interface?
See original GitHub issueWhen needing to attempt to bind an existing Interface to ZodObject
, there are a few issues with using the more simplistic ZodSchema
to achieve this - namely losing all the object-specific options such as schema.shape
.
As far as I know, there isn’t really a way to neatly infer a ZodObject
without needing to recapitulate the entire schema again within the definition, which is often not possible when the source type is external and the point of the binding is to notice if the shape of the schema differs from the source.
This is what I (very roughly) have worked out so far, so was wondering if a) there is any existing internals that might be better suited to this, and; b) if there is any chance some more developed version of this inference type might be possible to include in the main package?
/**
* Simple example mapping to string version of each type, would need to be completed with all types
*/
export type GetRequiredType<T> = T extends string
? "string"
: T extends number
? "number"
: T extends boolean
? "boolean"
: never
/**
* Append `Optional` to the key name for optional properties.
*/
export type GetType<T> = undefined extends T
? `${GetRequiredType<T>}Optional`
: GetRequiredType<T>
/**
* Simple example mapping / lookup of "rough" Zod types for each concrete type.
*/
export interface ZodTypes {
string: ZodString | ZodEffects<ZodString, string, any>
stringOptional: ZodOptional<ZodString | ZodEffects<ZodString, string, any>>
number: ZodNumber | ZodEffects<ZodNumber, number, any>
numberOptional: ZodOptional<ZodNumber | ZodEffects<ZodNumber, number, any>>
boolean: ZodBoolean | ZodEffects<ZodBoolean, boolean, any>
booleanOptional: ZodOptional<
ZodBoolean | ZodEffects<ZodBoolean, boolean, any>
>
}
/**
* Cast the existing output interface as a ZodRawShape using the lookups defined above.
*/
export type ToZodRawObject<Output extends object> = {
[Key in keyof Output]: ZodTypes[GetType<Output[Key]>]
}
/**
* Case the existing output interface as a valid ZodObject.
*/
export type ToZodObject<Output extends object> = ZodObject<
ToZodRawObject<Output>,
"strip",
ZodTypeAny,
Output
>
Passing Example
export interface User {
name?: string
email: string
}
export const UserSchema: ToZodObject<User> = z.object({
name: z.string().optional(), // Passes
email: z.string().email() // Passes
})
Failing Example
export interface Product {
name: string
serial?: number
}
export const ProductSchema: ToZodObject<Product> = z.object({
name: z.string().transform((value) => value.toUpperCase()), // Passes
serial: z.number().refine((value) => value > 1000) // Fails! (must be optional)
})
Issue Analytics
- State:
- Created a year ago
- Reactions:2
- Comments:8
Top GitHub Comments
IMHO this should be reopened, as
toZod
package is outdatedI can expand on the actual use-case, in a simple form :
Traditional zod usage
It often start with the schema defined. Later, the underlying, actual, data type of the object can be derived with
z.infer
Problematic usage
There are circumstances where the actual type is already defined and derived from somewhere else. A good example is a database model.
The actual trouble is, defining a random
z.object
in that case does not have the benefit to typecheck the schema versus the “wanted” type :A common workaround is to force the type, with
z.ZodType
:This works for autocompletion, but leads to several other issues. A good example is a real-world situation where you will re-use the schema in other schemas; such as creation or update payloads.
Consider the following
Pretty cool uh ? Here you can convince yourself it will throw on build, because
BearZ
here is actually not aZodObject
! (and.pick
relies onZodObject
).So let’s try using
ZodObject
directly maybe ? With something like :This doesn’t work written like this, because the index signature of
z.object
is not that simple :In that particular situation; I come to the conclusion that :
ZodObject
typeis impossible.
Reference : #53, #372
You might take a look at toZod, which is a simple utility that allows you to input a type or interface as a generic argument, and then type checks your Zod schema as you develop against the input type and throws a ts error if you don’t align. This makes your external type drive the Zod schema instead of the other way around, and you dont have to remember to keep them in sync. I’m using it with types generated from graphql-code-generator for mutation inputs and so far haven’t had any issues.