-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Support parametrisable schemas #153
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
Comments
This is possible! But generics are finicky as hell. Here's how to do it: export type Foo<T> = {
name: string;
value: T;
};
const fooSchema = <T extends z.ZodTypeAny>(
valueSchema: T,
): z.ZodObject<{ name: z.ZodString; value: T }> =>
z.object({
name: z.string(),
value: valueSchema,
});
const asdf = fooSchema(z.string()); Your issue was having your generic Btw your code works fine if you get rid of the return type annotation. TS can infer the output type correctly. export type Foo<T> = {
name: string
value: T
}
const fooSchema = <T>(valueSchema: z.ZodSchema<T>) => z.object({
name: z.string(),
value: valueSchema
}) I'm planning to add a guide to the README detailing how to build generic methods on top of Zod - should have done it a long time ago! |
Ah yes I see how you've done that. I should have clarified that I want to reference This is the reason I wanted to explicitly add the return type annotation to be Both code examples you gave are susceptible to this problem, unfortunately. In the end, we went with an approach like the second (i.e. omitting the return type annotation), and relying on some code in the tests that assigns the result of // Test code -- will not compile if fooSchema cannot be guaranteed to return a Foo
it('is able to assign the output of fooSchema(_).parse() to a variable of type Foo<_>', () => {
const raw: unknown = 'value not important'
try {
const parsed: Foo<string> = fooSchema(z.string()).parse(raw)
} finally { }
}) This is a bit of a hack, but I guess it works. It would be nice not to have to do that though. |
The reason this is hard is because it's the opposite of how Zod works. Normally it infer static type from validator structure, you want to "infer" validator structure from a static type. I talk about this concept a bit here: #53 (comment) As that comment mentions, I published a separate npm module |
I encountered this problem while trying to encode a import { z } from "zod";
export type Result<T, E> = {
success: true,
data: T
} | {
success: false,
error: E,
}
function zResult<Ok, Err>(
okSchema: z.Schema<Ok>,
errSchema: z.Schema<Err>
) {
return z.union([
z.object({
success: z.literal(true),
data: okSchema
}),
z.object({
success: z.literal(false),
error: errSchema
})
])
}
// ------ Test ---------
type Assert<T, Ok, Err> = T extends z.Schema<Result<Ok, Err >> ? true : false;
const _testSchema = zResult(z.number(), z.string());
const _testSchema_: Assert<typeof _testSchema, number, string> = true;
// this will show error
// const b: Assert<typeof testSchema, number, number> = true;
const _testSchema2 = zResult(z.object({foo: z.number()}), z.object({bar: z.string()}));
const _testSchema2_: Assert<typeof _testSchema2, {foo: number}, {bar: string}> = true;
// this will show error
// const _testSchema2__: Assert<typeof _testSchema2, {foo: number}, {baz: string}> = true; |
In fact, I don't have to type the above test cases, because the way I'm using the result of export type Parser<Out> = (data: unknown) => ParseResult<Out>;
export function zParser<Out, Def, In>(schema: z.Schema<Out, Def, In>): Parser<Out>;
export interface Endpoint<Req, Resp> {
path: string,
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
parseReq?: Parser<Req>,
parseResp: Parser<Resp>,
}
export namespace GetCanteens {
const okSchema = z.array(User);
export type Ok = z.infer<typeof okSchema>;
const errSchema = z.literal("UNAUTHORIZED");
export type Err = z.infer<typeof errSchema>;
const respParser = zParser(zFailable(okSchema, errSchema))
export type Resp = Result<Ok, Err>;
export const EP: Endpoint<never, Resp> = {
path: "/api/users/",
method: "GET",
parseResp: respParser,
}
} Since It feels like compile time duck typing. |
The problem appears to be the use of For now the workaround is to call the constructor of function makeParser2<T>(valueParser: z.ZodSchema<T>): z.ZodSchema<{v: T}>
{
const result: z.ZodSchema<{v: T}> = new z.ZodObject({
shape: () => ({
v: valueParser
}),
unknownKeys: 'strip',
catchall: z.any()
})
return result
} Note that the type of result has to be specified as |
If I've understood it correctly, I believe the upcoming TypeScript 4.7 feature "Instantiation Expressions" may make the following possible:
|
Sorry to bumping this but is there a reason on why this doesn't work with transforms? @colinhacks const fooSchema = <T extends z.ZodTypeAny>(valueSchema: T) =>
z
.object({
name: z.string(),
value: valueSchema,
})
.transform((foo) => foo.value);
// ^? Property 'value' does not exist on type '{ [k_1 in keyof addQuestionMarks<{ name: string; value: T["_output"]; }>]: addQuestionMarks<{ name: string; value: T["_output"]; }>[k_1]; }'. |
It would be cool to be able to do something like this:
(In fact it would be even cooler to be able to use
z.infer
on theReturnType<typeof fooSchema>
, but TypeScript is not yet capable of this -- see microsoft/TypeScript#40542)The above code produces the following compile error, which I have to be honest, is beyond my understanding:
The text was updated successfully, but these errors were encountered: