zod
zod copied to clipboard
z.function arguments can't be optional
// Case1
function demo(value?: boolean) {
console.log(value)
}
demo(); // work
const func = z
.function()
.args(z.boolean().optional())
.implement((val) => {
console.log(val);
});
func(); // fail
// Case2
function demo(arg1: number, arg2?: boolean) {
console.log(arg1, arg2)
}
demo(1); // work
const func = z
.function()
.args(z.number(), z.boolean().optional())
.implement((arg1, arg2) => {
console.log(arg1, arg2)
});
func(1); // fail
I tried it out in my own stackblitz
I looked it up and found that the function of optional()
is to make the type value: boolean | undefined
instead of value? : boolean
.
Then I looked at zod
, and I didn't find a way to implement z.boolean().partial()
, except that the ZodObject
type has a partial()
.
I think we should also implement a partial()
for function parameter types
I don't know what the developers are thinking about this, I would love to have more discussion, thank you
@tigerBeA Do you have any good solutions
I have looked through the implementation of optional()
, all primitive types use it, and the returned result is
primitive type | undefined
. If we add partial()
for primitive types, I think it will make users confused about when should use optional()
or partial()
. Secondly, partial()
makes all properties of the object optional, what should we expect partial()
for primitive types?
I tried to use optional primitive values in the object
const user = z.object({
username: z.string().optional(),
});
type test = z.infer<typeof user>;
// ^? { username?: string | undefined;}
Internally, we loop over key-value pairs detecting which key is optional to add the question mark, but in function, we receive an array of types, so I wonder do we have any solutions to detect and then add a question mark for parameter as in object? Besides that, we must ensure the valid order of optional/required parameters in the function.
@danh-luong I agree with you that it will make users confused about when should use optional() or partial()
Likewise,in function,I wonder do we have any solutions to detect and then add a question mark for parameter as in object?
So for now there is no way to pass a primitive argument as optional?
So for now there is no way to pass a primitive argument as optional?
I don't currently find it in function().args()
I don't think optional is correct here. ZodFunction.args
is a thin wrapper around ZodTuple
, which has a fixed length of distinctly typed arguments, then a rest
type of uniform type.
The best I think you can do is something like this:
const func = z.function(
z.tuple([]).or(z.tuple([z.boolean]))
).implement(arg=>{ /* your code here */})
That won't work; z.function()
wants a ZodTuple
and your example gives it a ZodUnion
. You'd have to do this sort of thing:
const func = z.function(z.tuple([])).or(z.function(z.tuple([z.boolean()])))
The types don't check, but I think it works at runtime (mumble grumble). Trouble with a union of functions is that you can't call .implement
on it. I also looked at z.tuple([]).rest(z.any()).refine(/*...*/)
which doesn't help with type hinting but does do runtime type checking.
I think that this is an area where there's no perfect solution, and zod
needs changes to truly support function overloads and/or optional arguments in a reasonable way.
@rotu I agree with you that zod
needs to change in this part. Are there zod
maintainers to participate in the discussion?
Having given up on implement()
, I finally settled on this awful hack, which typechecks and works at runtime (but would need tweaking if using any ZodEffect
s):
type MyFunc = (foo: string, bar?: string): string;
const foo = z.string().describe('A foo');
const bar = z.string().optional().describe('An optional bar');
const baz = z.string().describe('Bazzed');
const innerSchema = z.union([
z.function(z.tuple([foo, bar] as [foo: typeof foo, bar: typeof bar]), baz),
z.function(z.tuple([foo] as [foo: typeof foo]), baz)
]);
const myFuncSchema = z.custom<MyFunc>(value => innerSchema.safeParse(value).success);
As an added bonus, the MyFunc
type is readable. OTOH you'll also need to set up a custom error map.
I reproduced it in this TSPlayground:
import {z} from "zod";
const mySchemaWithObjectParameters = z.function()
.args(
z.object({
optional: z.string().optional(),
str: z.string(),
nullable: z.string().nullable(),
}).partial(),
z.object({
nullable: z.string().nullable(),
str: z.string(),
optional: z.string().optional(),
})
)
.returns(z.void());
// hover to see that the optional prop in both cases is
type MyFuncWithObjects = z.infer<typeof mySchemaWithObjectParameters>;
const mySchemaWithStrArg = z.function()
.args(
z.string().optional()
)
type MyFuncWithStr = z.infer<typeof mySchemaWithStrArg>;
// ?^ = (args_0: string | undefined, ...args_1: unknown[]) => unknown, not the expected: args_0?: string;
// see here the example of why .args(z.string().optional()) does not give the expected usage.
const fnWithQuestion = (str?: string) => str?.includes("hello");
const fnWithUndefined = (str: string | undefined) => str?.includes("world")
const hello = fnWithQuestion();
const world = fnWithUndefined();