colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
33.95k stars 1.19k forks source link

Unexpected output type in ZodDefault #3790

Closed hanakannzashi closed 4 weeks ago

hanakannzashi commented 1 month ago

ZodDefault use util.noUndefined<T["_output"] as output, which actually means Exclude<T["_output"], undefined>. If output is converted to undefined, e.g.

const checker = z.number().optional().transform((n) => {
  console.log(n); // n is 0 which means `default` is reachable
  return undefined; // return `undefined`
}).default(0);

// data got `undefined` without panic but actually return type of `parse` is `number` instead of `number | undefined` due to usage of `default`
const data = checker.parse(undefined); 

I think this is a type issue caused by util.noUndefined, maybe it should be undefined extends T ? never : T instead of T extends undefined ? never : T

sunnylost commented 4 weeks ago

data's type is undefined.

If you place default() last, it generates a default value and uses it as input for the preceding schema (which is a transform()). If you place transform() last, it will take the result from the previous schema and apply the transformation. In both cases, transform() has the highest priority and takes control of the final output.

hanakannzashi commented 4 weeks ago

data's type is undefined.

If you place default() last, it generates a default value and uses it as input for the preceding schema (which is a transform()). If you place transform() last, it will take the result from the previous schema and apply the transformation. In both cases, transform() has the highest priority and takes control of the final output.

Yes, I know how it works, I means, return type of parse is incorrect, TypeScript tell me that is a number, but actually I got undefined

sunnylost commented 4 weeks ago

Actually, when I pasted this code, I ended up with a never type.

hanakannzashi commented 4 weeks ago
const checker = z.number().optional().transform((n) => {
  console.log(n); // n is 0 which means `default` is reachable
  return undefined; // return `undefined`
}).default(0);

// data got `undefined` without panic but actually return type of `parse` is `number` instead of `number | undefined` due to usage of `default`
const data = checker.parse(undefined); 

Sorry my mistake