Open mmkal opened 1 year ago
Is this what you are looking for?
const userSchema = z.object( {
name: z.string().regex( /^\w+ \w+$/ ),
age: z.number(),
} )
type User = z.infer<typeof userSchema>
userSchema.parse( {
name: 'Bob',
birthdate: new Date( '1970-01-01' ),
} ) // runtime error, but no compile time error
userSchema.parse( {
name: 'Bob',
birthdate: new Date( '1970-01-01' ),
} as User ) // runtime and compile time error
Not exactly, because 1) as-casting isn't really safe, since it allows for missing properties and 2) it involves having a type reference for the schema, or creating one manually via z.infer<...>
.
Re 1) this would compile fine:
userSchema.parse({
name: 'Bob',
// no age!
} as User)
But maybe on latest typescript versions satisfies
would do the trick:
userSchema.parse({
name: 'Bob',
// no age!
} satisfies User) // compile time error
For 2) though, it's quite cumbersome when you don't already happen to have a type reference for the schema, e.g.:
loadAPISchemaInfo().lookupProcedure('user/create').getInputSchema().parse({
name: 'Bob',
} satisfies z.infer<???>)
The above code would need to be refactored to extract the schema into a variable, import {z} from 'zod'
, and then use z.infer<...>
. Those are all barriers on top of the temptation to just use .parse(...)
which accepts unknown
so will compile just fine - much less discoverable and much less likely to be used.
Would typedParse()
work for you?
const typedParse = <Schema extends z.ZodSchema> (
schema: Schema, data: z.infer<Schema>
): z.infer<Schema> => schema.parse( data )
const userSchema = z.object( {
name: z.string().regex( /^\w+ \w+$/ ),
age: z.number(),
} )
const user = typedParse( userSchema, {
name: 'Bob',
birthdate: new Date( '1970-01-01' ),
} ) // runtime and compile time error
// const user: {
// name: string
// age: number
// }
typedParse(
loadAPISchemaInfo().lookupProcedure( 'user/create' ).getInputSchema(),
{
name: 'Bob',
}
)
I don't think that this will ever be added to Zod, because it's my understanding that parse is meant to take something that is unknown and turn it into something that is known at compile time. So when you are trying to parse an object that is statically in the code base, that's something that is already known at compile time. In example code we do this all the time for learning/teaching purposes, but I don't know that I have ever done that in a real app. In real apps I use Zod for checking user input or a response from an api, which are things that can't be known at compile time. I hope this makes sense. Please let me know if you have questions or if I am misunderstanding what you are talking about.
Yes it's a slightly different use case than validating API inputs, and I usually want this when using refinement types, or regex strings, or similar, which perform runtime validation which is not reflected at compile time. And yes that helper function does the trick for most cases - what I'm asking is whether it could become part of zod. There are some edge cases like .transform types with a different input typearg which it'd be sensible to solve in one official place.
Just posted https://github.com/colinhacks/zod/issues/1892 that looks finally really the same than what @mmkal is looking for.
I totally agree with him on implementing it inside Zod. Since Typescript no longer exist at runtime, helping people in writing valid code at compilation helps.
In my case I write fixtures and Storybook stories with mocked data and it's sad Zod cannot tell me my object is wrong when it's missing 90% of the expected props. For sure it's not a magic silver bullet since Typescript wouldn't help with custom validation (min/max/regex...) but at least it helps on having the right properties, and the almost expected property types.
Another way you can solve this problem:
type TypedParseSchema<Schema extends z.ZodSchema> = Omit<Schema, 'parse'> & {
parse ( data: z.infer<Schema> ): z.infer<Schema>
}
const useTypedParse = <Schema extends z.ZodSchema>
( schema: Schema ): TypedParseSchema<Schema> => schema
const schemaWithTypedParse = useTypedParse( z.object( {
email: z.string(),
} ) )
schemaWithTypedParse.parse( 'hello' )
// ^^^^^^^
// Argument of type 'string' is not assignable to parameter of type '{ email: string; }'.
@mmkal and @sneko Would this work for you?
Yes it would. It's a bit sad to have to make "parser helpers" for each schema. Do you still think this should be kept outside the zod
library?
It's a bit sad to have to make "parser helpers" for each schema
What do you mean "for each schema"? The way I wrote it above can be reused on any schema. So you only need 1 useTypedParse
.
Do you still think this should be kept outside the
zod
library?
I don't know if it should be added in or not. I feel like this is up to @colinhacks.
I made a package that has a few utilities for zod and typed parse is one of them. Let me know if it helps. https://github.com/JacobWeisenburger/zod_utilz#usetypedparsers
What do you mean "for each schema"? The way I wrote it above can be reused on any schema. So you only need 1 useTypedParse.
By making "helpers" I mean if I have UserSchema
, CarSchema
... for each one I need to do:
const schemaWithXxxTypedParse = useTypedParse(XxxSchema)
(but I agree that's a decent workaround for now, even if I need to write it everywhere I have a schema.
I'm inclined to agree - absolutely, that helper works, but if the use case is valid enough for it to exist in a util library, then I'd argue it'd make sense to exist in zod too. It doesn't really add any meaningful maintenance burden, I don't think, since the implementation is just the same as the untyped parse
. A util library is less discoverable, requires separate installation and importing, might break with new versions etc. etc.
I totally agree. But I don't feel like I have the authority to add a feature like this to someone else's library. I am totally fine with maintaining it on my util library because I have the authority to do so.
https://github.com/JacobWeisenburger/zod_utilz#usetypedparsers should be part of zod. Very helpful! @colinhacks
I just wanted to voice my support for this. In addition to validating user inputs to my API, I'm using Zod to format responses from my API and I have tripped over this a few times where the type that I'm passing in obviously won't parse, like a Promise
if I mistakenly forgot to await
something, or passing in a T | null
or T | undefined
where I should have first checked if the value was there. While this isn't the main use-case for Zod, this feels like a really low-cost method to implement that would be useful for cases where you have semi-trusted input you want to parse.
@colinhacks now that zod is seeing more activity recently, what do you think?
Decided to put my money where my mouth is and put up https://github.com/colinhacks/zod/pull/3629
It'd be nice to have zod help the developer max out their chances of parsing a valid object, when it's manually constructed. Example:
The above code compiles, but we have enough information to know that it'll throw a ZodError at runtime. It'd be nice if there was a method like
Where the expected input type was
Input
(from theZodType<Output, Def, Input>
) rather thanunknown
. So the above code would error because the developer usedbirthdate
instead ofage
.This would be especially useful for types with a custom
.refine
or.regex
method or similar where it could be misleading to use just a type declaration:The above will compile, and implies that the
const user
is a validatedUser
instance, but it isn't becausename
doesn't match the regex. The current options are to do the above, which is safe-looking at compile time, but unsafe at runtime, or to use.parse(...)
which is unsafe at compile time and safe-ish at runtime (but will throw errors).