fabien0102 / ts-to-zod

Generate zod schemas from typescript types/interfaces
MIT License
1.23k stars 68 forks source link

Property shape does not exist on type ZodType #123

Open cbix opened 1 year ago

cbix commented 1 year ago

Bug description

ts-to-zod v3.0.0 generates a Zod schema that fails type verification with this error:

2339: Property 'shape' does not exist on type 'ZodType<test, ZodTypeDef, test>'.

Looking into the ZodType (exported as ZodSchema) definition it in fact doesn't have any field called shape.

Input

The input file is generated by openapi-typescript v6.2.0 but I could reproduce the issue with a type definition as simple as this:

// typescript type or interface causing the output
export interface test {
  schemas: {
    A: string | null;
    B: test['schemas']['A'][];
  };
}

Expected output

Not exactly sure ...

Actual output

// Actual Zod schemas
// Generated by ts-to-zod
import { z } from 'zod';
import { test } from './test.d';

export const testSchema: z.ZodSchema<test> = z.lazy(() =>
  z.object({
    schemas: z.object({
      A: z.string().nullable(),
      B: z.array(testSchema.shape.schemas.shape.A),
    }),
  }),
);

Versions

Andy-d-g commented 1 year ago

Hi, any update ?

cbix commented 1 year ago

Btw, this issue is not relevant to me anymore since we switched from openapi-typescript + ts-to-zod to openapi-zod-client.

0xjocke commented 1 month ago

We're still not supporting zod v3.21.4+?

shwaka commented 4 weeks ago

I have faced the same issue.

I have been looking into this issue, and while I haven't found a complete solution yet, I hope I may have uncovered something that could be helpful (at least for beginners like me).

Cause of the issue

The property shape is defined in the class ZodObject (types.ts), not in ZodType or ZodSchema.

In the case of the above example, since the input interface test is recursive, the output schema is wrapped by the function z.lazy, which returns ZodLazy. ZodLazy is not assignable to ZodObject nor has a field shape. So, to resolve the issue, we have to avoid the (ab)use of z.lazy.

Possible solutions

Here are two possible solutions. Although I have no idea how to achieve these automatically, but it is easy to apply these manually to your code.

Possible solution 1: Apply z.lazy to a small part

By editing the output as follows, the zod schema works fine.

export const testSchema: z.ZodObject<any, any, any, test> = // z.lazy(() =>
  z.object({
    schemas: z.object({
      A: z.string().nullable(),
      B: z.array(z.lazy(() => testSchema.shape.schemas.shape.A)), // z.lazy is moved to this line
    }),
  })/*,
)*/;

Applying z.lazy to a small part (instead of wrapping the whole object), we can replace ZodType with ZodObject and safely access to the field shape.

z.lazy is written by the function transformRecursiveSchema (transformRecursiveSchema.ts), which is called from the function generate in generate.ts. But it seems to be difficult (at least for me) to edit these functions so that the above code is emitted.

Possible solution 2: Avoid to use recursive object type

Edit the input as follows.

type AType = string | null;
type BType = AType[];
export interface test {
  schemas: {
    A: AType;
    B: BType;
  };
}

Then ts-to-zod emits the following zod schema, which works fine.

const aTypeSchema = z.string().nullable();
const bTypeSchema = z.array(aTypeSchema);

export const testSchema = z.object({
  schemas: z.object({
    A: aTypeSchema,
    B: bTypeSchema,
  }),
});

In order to use this solution in the specific situation of cbix, we need to add such feature to openapi-typescript (and I have no idea for that).