zod: `z.ZodType` and `refine` not working as expected

Consider the following code:

const stringIsValid = (value: string): value is "a" | "b" =>
  ["a", "b"].includes(value);

const ASchema = z.object({
  type: z.string().refine(stringIsValid),
});

type A = z.infer<typeof ASchema>;

interface B {
  id: string;
  children: (z.infer<typeof ASchema> | B)[];
}

const BSchema: z.ZodType<B> = z.lazy(() =>
  z.object({
    id: z.string(),
    children: z.array(z.union([ASchema, BSchema])),
  })
);

type A is correctly inferred as:

{
    type: "a" | "b";
}

But when creating the BSchema, and assigning it to z.ZodType<B> I get the following error:

Type 'ZodLazy<ZodObject<{ id: ZodString; children: ZodArray<ZodUnion<[ZodObject<{ type: ZodEffects<ZodString, "a" | "b", string>; }, "strip", ZodTypeAny, { type: "a" | "b"; }, { ...; }>, ZodType<...>]>, "many">; }, "strip", ZodTypeAny, { ...; }, { ...; }>>' is not assignable to type 'ZodType<B, ZodTypeDef, B>'.
  The types of '_input.children' are incompatible between these types.
    Type '({ type: string; } | B)[]' is not assignable to type '({ type: "a" | "b"; } | B)[]'.
      Type '{ type: string; } | B' is not assignable to type '{ type: "a" | "b"; } | B'.
        Type '{ type: string; }' is not assignable to type '{ type: "a" | "b"; } | B'.
          Type '{ type: string; }' is not assignable to type '{ type: "a" | "b"; }'.
            Types of property 'type' are incompatible.
              Type 'string' is not assignable to type '"a" | "b"'

Is this a bug or am I doing something weird? Zod-version is 3.20.2 and typescript version is 4.8.4

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 24 (8 by maintainers)

Most upvoted comments

Thanks for the help, it solve my issue (funny that i found it almost the same day as you).

I didn’t known that you could/should specify input type in this situation.

Adding an example in the docs would be a good thing

After some investigations, I realized I misunderstood some things, here is the proper way to solve the problem that @JoelBeeldi had!

 const idIsValid = (value: string): value is `${string}/${string}` =>
    value.split("/").length === 2;

  interface Entity {
    id: `${string}/${string}`;
    children: Entity[];
  }

  interface EntityInput {
    id: string;
    children: EntityInput[];
  }

  const EntitySchema: z.ZodType<Entity, z.ZodTypeDef, EntityInput> = z.lazy(
    () =>
      z.object({
        id: z.string().refine(idIsValid),
        children: z.array(EntitySchema),
      })
  );

Apparently, you need to define the “Input”-type of the schema, if you use refine/transform which is passed as the third type argument. Something we maybe should add to the docs. Might submit a PR for that another time!

Username too and thanks for the maintenance of this awesome lib

Username! Thanks

Nice! Thanks for your support @JacobWeisenburger ❤️

I understand now. I think I missed “would” when I read what you said.

I agree that this is something that should work without type casting or assertions.