Open twiddler opened 1 year ago
I think z.input
means "The type of a valid input" in the context of zod (the type before any transforms etc are made).
I believe you're correct that the input should include more types than just the type that the value is being coerced to.
But not all coercibles accept any type, for example if you do
// Errors
const parsed = z.coerce.number().parse("no");
// parsed is now the number 0
const parsed = z.coerce.number().parse(null);
So for z.coerce.number()
, it seems that z.input<...>
should be number | string | null
since those are each input types that could parse successfully (not sure if that's all the types).
For .string()
and .boolean()
I think that any
type should be returned from z.input
since in JS everything can be converted to a string or bool
z.input
should follow the specificiation, so IMHO:
number | undefined | null | boolean | string | Record<any, any>
symbol | bigint
(Interestingly, BigInt(Number(1))
does not throw a TypeError in Node 18. :thinking:)
I don't say it is bad do it based on spec, but I think first-place it should be based on what coerce of this library can work with. So if following the spec, then we need to make sure that coerce works based on spec, then the z.input would end up being exactly in line with it.
@iway1
For .string() and .boolean() I think that any type should be returned from z.input since in JS everything can be converted to a string or bool
Though maybe unknown
would be better. It makes sense following your example of coerce.number
having number | string | null
, which is an union, not intersection. That means if all types are coercible to string, it must be a union (not intersection) of all types. You can think of any as an intersection of all types with some intrinsic behavior, while unknown is an union.
It looks like this also makes it so that z.coerce.boolean().default(false)
doesn't work as expected. And using null
, undefined
, or 0
instead gives a typescript error.
This is what I'm doing as a workaround:
z.object({
SOME_BOOL_VALUE: z.string().default("false").transform(value => value !== "false"),
}).parse(process.env);
As a side note, since this is coming from an env, even something like SOME_BOOL_VALUE=0
or false
would really be a string and get treated as true
using coerce.boolean()
, so maybe this is the best we can do for booleans coming from strings.
@redbmk Unrelated to the original issue: Since you default to "false"
, it seems like that might be the "less risky" setting for your application, so maybe consider .transform(value => value === "true")
such that your boolean is only ever true when one really intends to.
I ran into this recently, ended up using a union of primitive types and calling the appropriate constructor in .transform
manually.
Posting it here as a workaround until this gets fixed in z.coerce
:
import { z } from 'zod'
const zStringToNumber = z.coerce.number()
type zIn1 = z.input<typeof zStringToNumber>
// ^?
const zStringToNumber2 = z.union([
z.string(), // maybe NaN
z.number(),
z.boolean(), // 0 or 1
z.bigint(),
z.date(),
z.symbol(), // throws
z.undefined(), // NaN
z.null() // 0
]).transform(v => Number(v))
type zIn2 = z.input<typeof zStringToNumber2>
// ^?
This is true. It was a tradeoff I made so I could ship z.coerce
without a breaking change. Because ZodString
has no generics, there's no way for me to change its _input
type without adding one, which would have been breaking. Another option would be to introduce some kind of ZodCoerce
type, but then you wouldn't be able to method calls z.coerce.string().min(5)
, etc.
This will be fixed in Zod 4. In the meantime you can work around it with .transform()
and .pipe()
as others have suggested.
In this code …
…
B
evaluates to …I expected
B
to evaluate to …If this is intended behaviour, I'd be happy if you could share your reasoning. Thank you. :pray:
This is related to #2421.
zod version: v3.21.4