-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Request for help: typed Zod combinator with dynamic field name #211
Comments
Good question. I actually couldn't figure this out either. Trying to write generic methods that accept Zod schemas is tricky. I ended up implementing a new method Here's how it's done: const XmlJsonArray = <Key extends string, Schema extends z.Schema<any>>(
element: Key,
schema: Schema,
) => {
return z.object({}).setKey(element, z.union([schema, z.array(schema)]));
};
const test = XmlJsonArray('asdf', z.string()); Parsing works as expected: // both work
test.parse({ asdf: 'hello' });
test.parse({ asdf: ['hello'] }); And type inference works too: Good question! |
This is brilliant, thank you for your help! |
Follow-up question: is there a reasonably concise way of representing the return type of const XmlJsonArray = <tag extends string, T>(
tag: tag,
schema: z.ZodType<T>
): z.ZodType<{[t in tag]: T | T[]}> => { // doesn't work
return z.object({}).setKey(tag, z.union([schema, z.array(schema)]));
}; I suspect that I'm running into a hard limitation of TypeScript here, but you'd know better than I. |
So you want to explicitly annotate the return type of the function? Any particular reason? In general things get funky when you use The actual way to type this requires more intimate knowledge of Zod. You need to tell TypeScript the full structure of the schema that gets returned from your function. Here's how to do it: import * as z from '.'
const XmlJsonArray = <T extends string, S extends z.ZodType<any>>(
tag: T,
schema: S,
): z.ZodObject<{ [k in T]: z.ZodUnion<[S, z.ZodArray<S>]> }> => {
return z.object({}).setKey(tag, z.union([schema, z.array(schema)]));
};
const mySchema = XmlJsonArray('tuna', z.string());
type mySchema = z.infer<typeof mySchema>
// => { tuna: string | string[]; } But I wouldn't recommend this. If you change your implementation you'll need to update the type signature accordingly. Best to let the type inference engine work it's magic here. Note: I made an important change to your code. When using generics, don't do this: const myFunc = <T>(schema: z.Schema<T>)=>{
// ...
} instead do this: const myFunc = <T extends z.Schema<any>>(schema: T)=>{
// ...
} This lets TypeScript infer the exact subclass of ZodType (e.g. ZodString, ZodObject, etc) instead of treating everything as an instance of ZodType (the base class for all Zod schemas). |
I'm trying to feed the parsed output of import * as z from 'zod';
const XmlJsonArray = <tag extends string, T, U extends z.Schema<T>>(tag: tag, schema: U) => {
return z.object({}).setKey(tag, z.union([schema, z.array(schema)]));
// or this syntax I just learned about
return z.object({[tag]: z.union([schema, z.array(schema)])});
};
// TODO the field in a should NOT be optional!
const formatXmlJsonArray = <tag extends string, T>(tag: tag, a: {[v in tag]?: T | T[]}): T[] => {
const elem: T | T[] = a[tag];
if (elem instanceof Array) return elem;
return [elem];
};
// Example usage
const numberParser = z.number();
// const zeroElements = '\r\n';
const oneElement = {Number: 1};
const manyElements = {Number: [1, 2, 3]};
// formatXmlJsonArray('Number', XmlJsonArray('Number', numberParser).parse(zeroElements)); // should return []
formatXmlJsonArray('Number', XmlJsonArray('Number', numberParser).parse(oneElement)); // should return [1]
formatXmlJsonArray('Number', XmlJsonArray('Number', numberParser).parse(manyElements)); // should return [1, 2, 3] TL;DR two things of interest:
|
I'm having trouble duplicating the Make sure you have I got this to work as (I think) you were intending. You need to use conditional types to get import * as z from '.';
const XmlJsonArray = <tag extends string, T, U extends z.Schema<T>>(
tag: tag,
schema: U,
) => {
return z.object({}).setKey(tag, z.union([schema, z.array(schema)]));
// or this syntax I just learned about
// return z.object({ [tag]: z.union([schema, z.array(schema)]) });
};
// TODO the field in a should NOT be optional!
const formatXmlJsonArray = <tag extends string, T>(
tag: tag,
a: { [v in tag]?: T },
): T extends Array<any> ? T : T[] => {
const elem = a[tag] as any;
if (elem instanceof Array) return elem as any;
return [elem] as any;
};
// Example usage
const numberParser = z.number();
// const zeroElements = '\r\n';
const oneElement = { Number: 1 };
const manyElements = { Number: [1, 2, 3] };
// formatXmlJsonArray('Number', XmlJsonArray('Number', numberParser).parse(zeroElements)); // should return []
const sample1 = formatXmlJsonArray(
'Number',
XmlJsonArray('Number', numberParser).parse(oneElement),
); // should return [1]
const sample2 = formatXmlJsonArray(
'Number',
XmlJsonArray('Number', numberParser).parse(manyElements),
); // should return [1, 2, 3] |
Yep, that was it. Thank you so much for your time! |
I apologize if this is silly, but just in case you missed it:
I suspect that this makes |
I was aware of this syntax but I couldn't get it to work regardless. If you consider this simple example: const test = <T extends string>(key:T)=>{
return {[key]: 25};
} TypeScript isn't able to infer the return type ( I was surprised by this actually. Anyway, I'm pretty sure |
I opened an issue on the TypeScript repo. Cheers! E: Ryan from the TypeScript team convinced me that this is working as intended, so I agree, |
Could you please document the For example, this works: const validationSchema = z.object({}).setKey('name', z.string().max(10)); But this does not: const validationSchema = z.object({})
for (const prop of someData) {
validationSchema.setKey(prop.name, z.string().max(10));
} And I need a way to loop through my data and define the schema. How can this be achieved? |
The problem you're running into is that const someData = [{ name: "foo" }, { name: "bar" }];
let schema = z.object({});
for (const prop of someData) {
schema = schema.setKey(prop.name, z.string());
} or if you prefer to avoid variable mutation: const schema = someData.reduce(
(schema, definition) => schema.setKey(definition.name, z.string()),
z.object({})
); |
Love it! |
No particular reason other than we just didn't write anything about it! I'll write something up later today, thanks for the encouragement. |
@scotttrinh I just looked but couldn't find anything about setKey in the docs. I don't see a JSDoc comment either. How's the documentation coming along? |
I was looking around for a solution to loop through and create a dynamic schema. Thank you for your solution @scotttrinh, I have solved my problem :D |
@scotttrinh unfortunately, the inferred type from the schema ends up being an empty object. Is there another way to loop through the keys and have the correct inferred type? |
I apologize, this is almost certainly not the correct place to ask, but I'm not sure where else.
I asked this question on Stack Overflow, reproducing it below.
My XML to JSON library emits
{MyKey: T}
for one-element lists, and{MyKey: T[]}
for multi-element lists. The corresponding TypeScript type istype XmlJsonArray<T, element extends string> = Record<element, T | T[]>
. I've used the following to implement it as a Zod schema:Is there a way to do this without using
any
?The text was updated successfully, but these errors were encountered: