colinhacks / tozod

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

toZod (v3.0.0) does not infer nullable+optional (nullish) number type properly

waful opened this issue · comments

In my source interface I have:

interface ContrivedInterface {
  problematicNumber?: number | null;
}

I would write it in zod as follows:

const contrivedZod: toZod<ContrivedInterface> = z.object({
  problematicNumber: z.number().nullable().optional()
})

Problem is, the type of problematicNumber from z.infer is z.ZodOptional<z.ZodNullable<z.ZodNumber>>, whereas the type of problematicNumber generated by toZod<ContrivedInterface> is ZodOptional<never> | ZodOptional<ZodNumber> causing a type mismatch.

The exact ts error:

Type '{ problematicNumber: z.ZodOptional<z.ZodNullable<z.ZodNumber>>; }' is not assignable to type '{ problematicNumber: ZodOptional<never> | ZodOptional<ZodNumber>; }'.

My usecase is, I want to be able to send null from client to server as an indicator that I want problematicNumber to be unset on backend because undefined is not a valid JSON value so I can't use it in my payload. So undefined represents a lack of value whereas null represents intentional removal of value.

I'm having the same problem, did you solve it?

@sebastiangon11 As suggested by some of the other issues, I replaced zod with superstruct and found no issues.

https://docs.superstructjs.org/api-reference/typescript

commented

I ran into the same issue, where toZod would not recognize that T | null | undefined is in fact a nullable T.
Here is a small change that fixes this:

import * as z from 'zod';
declare type isAny<T> = [any extends T ? 'true' : 'false'] extends ['true'] ? true : false;
declare type equals<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false;

type MakeNullish<T> = z.ZodOptional<z.ZodNullable<toZod<Exclude<T, undefined | null>>>>

type isNullish<T> =
  null extends T
    ? undefined extends T
      ? true
      : false
    : false

export declare type toZod<T> =
  isAny<T> extends true
    ? never
    : [T] extends [boolean]
      ? z.ZodBoolean
      : isNullish<T> extends true
        ? MakeNullish<T>
        : [undefined] extends [T]
          ? T extends undefined
            ? never
            : z.ZodOptional<toZod<T>>
          : [null] extends [T]
            ? T extends null
              ? never
              : z.ZodNullable<toZod<T>>
            : T extends Array<infer U>
              ? z.ZodArray<toZod<U>>
              : T extends Promise<infer U>
                ? z.ZodPromise<toZod<U>>
                : equals<T, string> extends true
                  ? z.ZodString : equals<T, bigint> extends true
                    ? z.ZodBigInt
                    : equals<T, number> extends true
                      ? z.ZodNumber
                      : equals<T, Date> extends true
                        ? z.ZodDate
                        : T extends {
                            [k: string]: any;
                          }
                          ? z.ZodObject<{
                            [k in keyof T]-?: toZod<T[k]>;
                          }, 'strip', z.ZodTypeAny, T, T>
                          : never;