colinhacks / zod

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

Incorrect type derivation when using z.array() with z.transform() #3735

Closed jwatte closed 2 weeks ago

jwatte commented 2 weeks ago

In a much bigger project, I have a compile error where it complains that a type "string | boolean" can't be assigned to "boolean." I want to define a boolean field that can coerce from string. Unfortunately, "coerce" doesn't work, because the string "false" is coerced to true, because it's JavaScript truthy. Thus, I tried using transform() instead.

Here is the small reproduction case:

import { z } from "zod";

export const ZInner = z.strictObject({
        a_string: z.string(),
        a_boolean: z.boolean().or(
                z.string().transform((v, ctx) => {
                        if (v === "true") return true;
                        if (v === "false" || v === "") return false;
                        ctx.addIssue({ code: z.ZodIssueCode.custom, message: "invalid boolean value: " + v });
                        return false;
                })
        ),
});
export type IInner = z.infer<typeof ZInner>;

export const ZOuterList: z.ZodType<IInner[]> = z.lazy(() => z.array(ZInner));
export type IOuterList = z.infer<typeof ZOuterList>;

This gives the following error:

> tsc --build --pretty

src/index.ts:16:14 - error TS2322: Type 'ZodLazy<ZodArray<ZodObject<{ a_string: ZodString; a_boolean: ZodUnion<[ZodBoolean, ZodEffects<ZodString, boolean, string>]>; }, "strict", ZodTypeAny, { ...; }, { ...; }>, "many">>' is not assignable to type 'ZodType<{ a_string?: string; a_boolean?: boolean; }[], ZodTypeDef, { a_string?: string; a_boolean?: boolean; }[]>'.
  Types of property '_input' are incompatible.
    Type '{ a_string?: string; a_boolean?: string | boolean; }[]' is not assignable to type '{ a_string?: string; a_boolean?: boolean; }[]'.
      Type '{ a_string?: string; a_boolean?: string | boolean; }' is not assignable to type '{ a_string?: string; a_boolean?: boolean; }'.
        Types of property 'a_boolean' are incompatible.
          Type 'string | boolean' is not assignable to type 'boolean'.
            Type 'string' is not assignable to type 'boolean'.

16 export const ZOuterList: z.ZodType<IInner[]> = z.lazy(() => z.array(ZInner));

It is my assumption that, if IInner is boolean-only, then z.array(ZInner) should also be. However, this doesn't seem to hold true.

jwatte commented 2 weeks ago

Also, the lazy() doesn't matter (happens without it,) and trying to tack on .pipe(z.coerce.boolean()) doesn't work, same error.

jwatte commented 2 weeks ago

Looking at this some more, the problem here is that the z.ZodType<Output, Def, Input> typedef defaults Input to Output and in this case, Input is something else. So, I have to indirect another type for the z.input<typeof ZInner> and pass that to the ZodType definition, and then it works.