colinhacks / zod

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

Refine removes null from schema #3597

Open 3pleFly opened 2 months ago

3pleFly commented 2 months ago

Hello. I don't quite understand what happens but when I use typescript 5.5 (does not happen with 5.2)the refine removes null from union in my schema. Proof: image

If I remove the refine segment, now null is added to the type union and the error is gone. image

If I switch the refine function arguement with a boolean constructor,then the error is gone, but that is because typescript does not use Boolean as a type predicate, so perhaps this further proves that ts's 5.5 inferred predicates work on refine now too. image

So obviously refine is doing something here... But, I am unable to duplicate this error when I upload it on stackblitz, here - https://stackblitz.com/edit/typescript-ebur5x?file=index.ts

I am not sure what is going on... Why is refine suddenly removing nulls from the union? it doesn't do it in older typescript versions, and I can't seem to replicate this in stackblitz so maybe it's just me? I guess the question is why is refine removing the null from my union and how can I make it stop... I am using strict mode.

Jack-W1234 commented 2 months ago

I can also confirm the same issue updating to TS 5.5

Suddenly have errors across the application from otherwise perfectly functional forms.

In my case, the required functionality is to allow null as a starting value for a field, but require a value at submission time. Previously I would use this approach as it contained all logic within the field definition :

z.object({
    Field1: z
      .enum(Field1Options)
      .nullable()
      .refine((value) => value !== null, {
        message: "Please select an option",
      }),
  })

Now. Ts warns that null is not a valid default value as the infered schema no longer includes null.

The workaround now is to move all refines to the form-level, so would be:

z.object({
    Field1: z
      .enum(Field1Options)
      .nullable()
  })
  .refine((values) => values.Field1!== null, {
    path: ["Field1"],
    message: "Please select an option",
  })

Was my previous method just bad practice? It seems more confusing to chuck all that logic at the end now.

EDIT: Also using strict mode

3pleFly commented 2 months ago

I can also confirm the same issue updating to TS 5.5

Suddenly have errors across the application from otherwise perfectly functional forms.

In my case, the required functionality is to allow null as a starting value for a field, but require a value at submission time. Previously I would use this approach as it contained all logic within the field definition :

z.object({
    Field1: z
      .enum(Field1Options)
      .nullable()
      .refine((value) => value !== null, {
        message: "Please select an option",
      }),
  })

Now. Ts warns that null is not a valid default value as the infered schema no longer includes null.

The workaround now is to move all refines to the form-level, so would be:

z.object({
    Field1: z
      .enum(Field1Options)
      .nullable()
  })
  .refine((values) => values.Field1!== null, {
    path: ["Field1"],
    message: "Please select an option",
  })

Was my previous method just bad practice? It seems more confusing to chuck all that logic at the end now.

EDIT: Also using strict mode

You can also use the boolean constructor as I've demonstrated in my post. highly appreciate your commentary :)