Repository: fabien0102/ts-to-zod
Pull request #85 (2022-06-07 08:58:35)

feat: Generate inferred types
## Why

If you want to tweak the generated zod schemas, you can't used the original types (they will be out of sync).

For supporting this usecase, this PR is adding a `inferredTypes` options to provide a file path.

## Note

We are generating inferred types in a separate file to avoid any name conflicts, indeed, when you have a circular types, we are using the original type as hint for zod.

## Related issue

#81 



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 97b1044..7443fc4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,54 @@
 # Changelog
 
+### [2.0.1](https://www.github.com/fabien0102/ts-to-zod/compare/v2.0.0...v2.0.1) (2023-01-30)
+
+
+### Bug Fixes
+
+* empty interfaces' extends statements are ignored [#108](https://www.github.com/fabien0102/ts-to-zod/issues/108) ([#109](https://www.github.com/fabien0102/ts-to-zod/issues/109)) ([4ad2d09](https://www.github.com/fabien0102/ts-to-zod/commit/4ad2d0962fd1a5efd1b14e4b89d2b642c227649a))
+
+## [2.0.0](https://www.github.com/fabien0102/ts-to-zod/compare/v1.13.1...v2.0.0) (2023-01-26)
+
+
+### ⚠ BREAKING CHANGES
+
+* Inheritance and reference type search for name filtering (#104)
+
+### Features
+
+* Inheritance and reference type search for name filtering ([#104](https://www.github.com/fabien0102/ts-to-zod/issues/104)) ([038b9f6](https://www.github.com/fabien0102/ts-to-zod/commit/038b9f6c14df79d9fc9756f6c2c21d76e8c46cfe))
+
+
+### Bug Fixes
+
+* extends interface inside namespace ([#106](https://www.github.com/fabien0102/ts-to-zod/issues/106)) ([958d5a5](https://www.github.com/fabien0102/ts-to-zod/commit/958d5a59e8df9b6f0183f64f794eaf26eb5350a8))
+
+### [1.13.1](https://www.github.com/fabien0102/ts-to-zod/compare/v1.13.0...v1.13.1) (2022-08-26)
+
+
+### Bug Fixes
+
+* Fix bad cherry-pick ([ac5af38](https://www.github.com/fabien0102/ts-to-zod/commit/ac5af38a2737a52b707d287077397c9ad8314b6b))
+
+## [1.13.0](https://www.github.com/fabien0102/ts-to-zod/compare/v1.12.0...v1.13.0) (2022-08-26)
+
+
+### Features
+
+* add support for  `z.set` ([#94](https://www.github.com/fabien0102/ts-to-zod/issues/94)) ([762c69c](https://www.github.com/fabien0102/ts-to-zod/commit/762c69c1f8ed89d435251ee415dc7e2249a951f9))
+
+
+### Bug Fixes
+
+* Fix nullable ([#92](https://www.github.com/fabien0102/ts-to-zod/issues/92)) ([f2321a3](https://www.github.com/fabien0102/ts-to-zod/commit/f2321a355910418ddfb12cb93fcd4b4590469e68))
+
+## [1.12.0](https://www.github.com/fabien0102/ts-to-zod/compare/v1.11.0...v1.12.0) (2022-07-26)
+
+
+### Features
+
+* Adds support and test for ReadonlyArray ([#88](https://www.github.com/fabien0102/ts-to-zod/issues/88)) ([513ebf9](https://www.github.com/fabien0102/ts-to-zod/commit/513ebf9134375960c721a6acf1ad5a78d1abf92a))
+
 ## [1.11.0](https://www.github.com/fabien0102/ts-to-zod/compare/v1.10.0...v1.11.0) (2022-06-02)
 
 
diff --git a/README.md b/README.md
index 68fbc85..0d766a4 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ List of supported keywords:
 | `@minimum {number}`                | `@minimum 42`     | `z.number().min(42)`         |
 | `@maximum {number}`                | `@maximum 42`     | `z.number().max(42)`         |
 | `@minLength {number}`              | `@minLength 42`   | `z.string().min(42)`         |
-| `@maxLength {number}`              | `@maxLength 42`   | `z.string().min(42)`         |
+| `@maxLength {number}`              | `@maxLength 42`   | `z.string().max(42)`         |
 | `@format {"email"\|"uuid"\|"url"}` | `@format email`   | `z.string().email()`         |
 | `@pattern {regex}`                 | `@pattern ^hello` | `z.string().regex(/^hello/)` |
 
@@ -179,14 +179,13 @@ export interface Superman {
 }
 ```
 
-/!\ Please note: if your exported interface/type have a reference to a non-exported interface/type, ts-to-zod will not be able to generate anything (a circular dependency will be report due to the missing reference).
+/!\ Please note: if your exported interface/type have a reference to a non-exported interface/type, ts-to-zod will not be able to generate anything (missing dependencies will be reported).
 
 ## Limitation
 
 Since we are generating Zod schemas, we are limited by what Zod actually supports:
 
 - No type generics
-- No complex circular dependencies (you will be warn if you have some in your types)
 - No `Record<number, …>`
 - …
 
diff --git a/example/heros.types.ts b/example/heros.types.ts
new file mode 100644
index 0000000..0b39fb5
--- /dev/null
+++ b/example/heros.types.ts
@@ -0,0 +1,36 @@
+// Generated by ts-to-zod
+import { z } from "zod";
+
+import * as generated from "./heros.zod";
+
+export type EnemyPower = z.infer<typeof generated.enemyPowerSchema>;
+
+export type SkillsSpeedEnemy = z.infer<typeof generated.skillsSpeedEnemySchema>;
+
+export type Enemy = z.infer<typeof generated.enemySchema>;
+
+export type Superman = z.infer<typeof generated.supermanSchema>;
+
+export type Villain = z.infer<typeof generated.villainSchema>;
+
+export type Story = z.infer<typeof generated.storySchema>;
+
+export type KrytonResponse = z.infer<typeof generated.krytonResponseSchema>;
+
+export type KillSuperman = z.infer<typeof generated.killSupermanSchema>;
+
+export type WithDefaults = z.infer<typeof generated.withDefaultsSchema>;
+
+export type Exported = z.infer<typeof generated.exportedSchema>;
+
+export type GetSupermanSkill = z.infer<typeof generated.getSupermanSkillSchema>;
+
+export type HeroContact = z.infer<typeof generated.heroContactSchema>;
+
+export type SupermanEnemy = z.infer<typeof generated.supermanEnemySchema>;
+
+export type SupermanName = z.infer<typeof generated.supermanNameSchema>;
+
+export type SupermanInvinciblePower = z.infer<
+  typeof generated.supermanInvinciblePowerSchema
+>;
diff --git a/example/heros.zod.ts b/example/heros.zod.ts
index 076b4ac..a5ac122 100644
--- a/example/heros.zod.ts
+++ b/example/heros.zod.ts
@@ -1,6 +1,6 @@
 // Generated by ts-to-zod
 import { z } from "zod";
-import { EnemyPower, Villain } from "./heros";
+import { EnemyPower, Villain, EvilPlan, EvilPlanDetails } from "./heros";
 
 export const enemyPowerSchema = z.nativeEnum(EnemyPower);
 
@@ -89,3 +89,18 @@ export const supermanNameSchema = supermanSchema.shape.name;
 
 export const supermanInvinciblePowerSchema =
   supermanSchema.shape.powers.items[2];
+
+export const evilPlanSchema: z.ZodSchema<EvilPlan> = z.lazy(() =>
+  z.object({
+    owner: villainSchema,
+    description: z.string(),
+    details: evilPlanDetailsSchema,
+  })
+);
+
+export const evilPlanDetailsSchema: z.ZodSchema<EvilPlanDetails> = z.lazy(() =>
+  z.object({
+    parent: evilPlanSchema,
+    steps: z.array(z.string()),
+  })
+);
diff --git a/package.json b/package.json
index 0e430bf..8e8d7f9 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "ts-to-zod",
   "description": "Generate zod schemas from typescript types/interfaces",
-  "version": "1.11.0",
+  "version": "2.0.1",
   "homepage": "https://github.com/fabien0102/ts-to-zod",
   "bugs": "https://github.com/fabien0102/ts-to-zod/issues",
   "repository": "fabien0102/ts-to-zod",
@@ -39,6 +39,7 @@
   "dependencies": {
     "@oclif/command": "^1.8.6",
     "@oclif/config": "^1.17.1",
+    "@oclif/errors": "^1.3",
     "@oclif/plugin-help": "^3.2.7",
     "@typescript/vfs": "^1.3.5",
     "async": "^3.2.0",
@@ -81,7 +82,7 @@
     "jest": "^27.5.1",
     "pretty-quick": "^3.1.0",
     "rimraf": "^3.0.2",
-    "ts-node": "^9.1.1"
+    "ts-node": "^10.9.1"
   },
   "jest": {
     "testEnvironment": "node",
diff --git a/src/cli.ts b/src/cli.ts
index 2326096..a915462 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -43,7 +43,7 @@ try {
     oclifError(
       `"${tsToZodConfigJs}" invalid:
   ${e.message}
-  
+
   Please fix the invalid configuration
   You can generate a new config with --init`,
       { exit: false }
@@ -67,11 +67,6 @@ class TsToZod extends Command {
   static flags = {
     version: flags.version({ char: "v" }),
     help: flags.help({ char: "h" }),
-    maxRun: flags.integer({
-      hidden: true,
-      default: 10,
-      description: "max iteration number to resolve the declaration order",
-    }),
     keepComments: flags.boolean({
       char: "k",
       description: "Keep parameters comments",
@@ -88,6 +83,9 @@ class TsToZod extends Command {
       default: false,
       description: "Skip the validation step (not recommended)",
     }),
+    inferredTypes: flags.string({
+      description: "Path of z.infer<> types file",
+    }),
     watch: flags.boolean({
       char: "w",
       default: false,
@@ -238,21 +236,22 @@ See more help with --help`,
       sourceText,
       ...fileConfig,
     };
-    if (typeof flags.maxRun === "number") {
-      generateOptions.maxRun = flags.maxRun;
-    }
     if (typeof flags.keepComments === "boolean") {
       generateOptions.keepComments = flags.keepComments;
     }
     if (typeof flags.skipParseJSDoc === "boolean") {
       generateOptions.skipParseJSDoc = flags.skipParseJSDoc;
     }
+    if (typeof flags.inferredTypes === "string") {
+      generateOptions.inferredTypes = flags.inferredTypes;
+    }
 
     const {
       errors,
       transformedSourceText,
       getZodSchemasFile,
       getIntegrationTestFile,
+      getInferredTypes,
       hasCircularDependencies,
     } = generate(generateOptions);
 
@@ -303,6 +302,27 @@ See more help with --help`,
 
     const prettierConfig = await prettier.resolveConfig(process.cwd());
 
+    if (generateOptions.inferredTypes) {
+      const zodInferredTypesFile = getInferredTypes(
+        getImportPath(generateOptions.inferredTypes, outputPath)
+      );
+      await outputFile(
+        generateOptions.inferredTypes,
+        prettier.format(
+          hasExtensions(generateOptions.inferredTypes, javascriptExtensions)
+            ? ts.transpileModule(zodInferredTypesFile, {
+                compilerOptions: {
+                  target: ts.ScriptTarget.Latest,
+                  module: ts.ModuleKind.ESNext,
+                  newLine: ts.NewLineKind.LineFeed,
+                },
+              }).outputText
+            : zodInferredTypesFile,
+          { parser: "babel-ts", ...prettierConfig }
+        )
+      );
+    }
+
     if (output && hasExtensions(output, javascriptExtensions)) {
       await outputFile(
         outputPath,
diff --git a/src/config.ts b/src/config.ts
index 7d53b34..6c78770 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -34,11 +34,6 @@ export type Config = {
    */
   skipValidation?: boolean;
 
-  /**
-   * Max iteration number to resolve the declaration order.
-   */
-  maxRun?: number;
-
   /**
    * Filter on type/interface name.
    */
@@ -66,6 +61,11 @@ export type Config = {
    * @default false
    */
   skipParseJSDoc?: boolean;
+
+  /**
+   * Path of z.infer<> types file.
+   */
+  inferredTypes?: string;
 };
 
 export type Configs = Array<
diff --git a/src/config.zod.ts b/src/config.zod.ts
index 5c4e090..6910164 100644
--- a/src/config.zod.ts
+++ b/src/config.zod.ts
@@ -25,12 +25,12 @@ export const configSchema = z.object({
   input: z.string(),
   output: z.string(),
   skipValidation: z.boolean().optional(),
-  maxRun: z.number().optional(),
   nameFilter: nameFilterSchema.optional(),
   jsDocTagFilter: jSDocTagFilterSchema.optional(),
   getSchemaName: getSchemaNameSchema.optional(),
   keepComments: z.boolean().optional().default(false),
   skipParseJSDoc: z.boolean().optional().default(false),
+  inferredTypes: z.string().optional(),
 });
 
 export const configsSchema = z.array(
diff --git a/src/core/generate.ts b/src/core/generate.ts
index 2776cae..4ac54ff 100644
--- a/src/core/generate.ts
+++ b/src/core/generate.ts
@@ -1,9 +1,14 @@
 import { camel } from "case";
 import { getJsDoc } from "tsutils";
 import ts from "typescript";
-import { resolveModules } from "../utils/resolveModules";
 import { JSDocTagFilter, NameFilter } from "../config";
 import { getSimplifiedJsDocTags } from "../utils/getSimplifiedJsDocTags";
+import { resolveModules } from "../utils/resolveModules";
+import {
+  getExtractedTypeNames,
+  isTypeNode,
+  TypeNode,
+} from "../utils/traverseTypes";
 import { generateIntegrationTests } from "./generateIntegrationTests";
 import { generateZodInferredType } from "./generateZodInferredType";
 import { generateZodSchemaVariableStatement } from "./generateZodSchema";
@@ -15,11 +20,6 @@ export interface GenerateProps {
    */
   sourceText: string;
 
-  /**
-   * Max iteration number to resolve the declaration order.
-   */
-  maxRun?: number;
-
   /**
    * Filter on type/interface name.
    */
@@ -47,6 +47,11 @@ export interface GenerateProps {
    * @default false
    */
   skipParseJSDoc?: boolean;
+
+  /**
+   * Path of z.infer<> types file.
+   */
+  inferredTypes?: string;
 }
 
 /**
@@ -56,7 +61,6 @@ export interface GenerateProps {
  */
 export function generate({
   sourceText,
-  maxRun = 10,
   nameFilter = () => true,
   jsDocTagFilter = () => true,
   getSchemaName = (id) => camel(id) + "Schema",
@@ -67,10 +71,19 @@ export function generate({
   const sourceFile = resolveModules(sourceText);
 
   // Extract the nodes (interface declarations & type aliases)
-  const nodes: Array<
-    ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.EnumDeclaration
-  > = [];
+  const nodes: Array<TypeNode> = [];
+
+  // declare a map to store the interface name and its corresponding zod schema
+  const typeNameMapping = new Map<string, TypeNode>();
+
+  const typesNeedToBeExtracted = new Set<string>();
 
+  const typeNameMapBuilder = (node: ts.Node) => {
+    if (isTypeNode(node)) {
+      typeNameMapping.set(node.name.text, node);
+    }
+  };
+  ts.forEachChild(sourceFile, typeNameMapBuilder);
   const visitor = (node: ts.Node) => {
     if (
       ts.isInterfaceDeclaration(node) ||
@@ -81,11 +94,26 @@ export function generate({
       const tags = getSimplifiedJsDocTags(jsDoc);
       if (!jsDocTagFilter(tags)) return;
       if (!nameFilter(node.name.text)) return;
-      nodes.push(node);
+
+      const typeNames = getExtractedTypeNames(
+        node,
+        sourceFile,
+        typeNameMapping
+      );
+      typeNames.forEach((typeName) => {
+        typesNeedToBeExtracted.add(typeName);
+      });
     }
   };
   ts.forEachChild(sourceFile, visitor);
 
+  typesNeedToBeExtracted.forEach((typeName) => {
+    const node = typeNameMapping.get(typeName);
+    if (node) {
+      nodes.push(node);
+    }
+  });
+
   // Generate zod schemas
   const zodSchemas = nodes.map((node) => {
     const typeName = node.name.text;
@@ -101,6 +129,7 @@ export function generate({
 
     return { typeName, varName, ...zodSchema };
   });
+  const zodSchemaNames = zodSchemas.map(({ varName }) => varName);
 
   // Resolves statements order
   // A schema can't be declared if all the referenced schemas used inside this one are not previously declared.
@@ -110,17 +139,31 @@ export function generate({
   >();
   const typeImports: Set<string> = new Set();
 
-  let n = 0;
-  while (statements.size !== zodSchemas.length && n < maxRun) {
+  // Zod schemas with direct or indirect dependencies that are not in `zodSchemas`, won't be generated
+  const zodSchemasWithMissingDependencies = new Set<string>();
+
+  let done = false;
+  // Loop until no more schemas can be generated and no more schemas with direct or indirect missing dependencies are found
+  while (
+    !done &&
+    statements.size + zodSchemasWithMissingDependencies.size !==
+      zodSchemas.length
+  ) {
+    done = true;
     zodSchemas
-      .filter(({ varName }) => !statements.has(varName))
+      .filter(
+        ({ varName }) =>
+          !statements.has(varName) &&
+          !zodSchemasWithMissingDependencies.has(varName)
+      )
       .forEach(
         ({ varName, dependencies, statement, typeName, requiresImport }) => {
           const isCircular = dependencies.includes(varName);
-          const missingDependencies = dependencies
+          const notGeneratedDependencies = dependencies
             .filter((dep) => dep !== varName)
             .filter((dep) => !statements.has(dep));
-          if (missingDependencies.length === 0) {
+          if (notGeneratedDependencies.length === 0) {
+            done = false;
             if (isCircular) {
               typeImports.add(typeName);
               statements.set(varName, {
@@ -133,24 +176,45 @@ export function generate({
               }
               statements.set(varName, { value: statement, typeName });
             }
+          } else if (
+            // Check if every dependency is (in `zodSchemas` and not in `zodSchemasWithMissingDependencies`)
+            !notGeneratedDependencies.every(
+              (dep) =>
+                zodSchemaNames.includes(dep) &&
+                !zodSchemasWithMissingDependencies.has(dep)
+            )
+          ) {
+            done = false;
+            zodSchemasWithMissingDependencies.add(varName);
           }
         }
       );
-
-    n++; // Just a safety net to avoid infinity loops
   }
 
-  // Warn the user of possible not resolvable loops
-  const missingStatements = zodSchemas.filter(
-    ({ varName }) => !statements.has(varName)
-  );
+  // Generate remaining schemas, which have circular dependencies with loop of length > 1 like: A->B—>C->A
+  zodSchemas
+    .filter(
+      ({ varName }) =>
+        !statements.has(varName) &&
+        !zodSchemasWithMissingDependencies.has(varName)
+    )
+    .forEach(
+      ({ varName, dependencies, statement, typeName, requiresImport }) => {
+        typeImports.add(typeName);
+        statements.set(varName, {
+          value: transformRecursiveSchema("z", statement, typeName),
+          typeName,
+        });
+      }
+    );
 
+  // Warn the user of possible not resolvable loops
   const errors: string[] = [];
 
-  if (missingStatements.length) {
+  if (zodSchemasWithMissingDependencies.size > 0) {
     errors.push(
-      `Some schemas can't be generated due to circular dependencies:
-${missingStatements.map(({ varName }) => `${varName}`).join("\n")}`
+      `Some schemas can't be generated due to direct or indirect missing dependencies:
+${Array.from(zodSchemasWithMissingDependencies).join("\n")}`
     );
   }
 
@@ -220,9 +284,31 @@ ${Array.from(statements.values())
     return print(zodInferredSchema);
   })
   .join("\n\n")}
+
 ${testCases.map(print).join("\n")}
 `;
 
+  const getInferredTypes = (
+    zodSchemasImportPath: string
+  ) => `// Generated by ts-to-zod
+import { z } from "zod";
+
+import * as generated from "${zodSchemasImportPath}";
+
+${Array.from(statements.values())
+  .filter(isExported)
+  .map((statement) => {
+    const zodInferredSchema = generateZodInferredType({
+      aliasName: statement.typeName,
+      zodConstName: `generated.${getSchemaName(statement.typeName)}`,
+      zodImportValue: "z",
+    });
+
+    return print(zodInferredSchema);
+  })
+  .join("\n\n")}
+`;
+
   return {
     /**
      * Source text with pre-process applied.
@@ -244,6 +330,13 @@ ${testCases.map(print).join("\n")}
      */
     getIntegrationTestFile,
 
+    /**
+     * Get the content of the zod inferred types files.
+     *
+     * @param zodSchemasImportPath Relative path of the zod schemas file
+     */
+    getInferredTypes,
+
     /**
      * List of generation errors.
      */
diff --git a/src/core/generateZodSchema.ts b/src/core/generateZodSchema.ts
index 3174098..cf58008 100644
--- a/src/core/generateZodSchema.ts
+++ b/src/core/generateZodSchema.ts
@@ -1,14 +1,14 @@
 import { camel, lower } from "case";
+import uniq from "lodash/uniq";
 import * as ts from "typescript";
+import { findNode } from "../utils/findNode";
+import { isNotNull } from "../utils/isNotNull";
 import {
   getJSDocTags,
   JSDocTags,
   jsDocTagToZodProperties,
   ZodProperty,
 } from "./jsDocTags";
-import uniq from "lodash/uniq";
-import { findNode } from "../utils/findNode";
-import { isNotNull } from "../utils/isNotNull";
 
 const { factory: f } = ts;
 
@@ -256,6 +256,7 @@ function buildZodPrimitive({
         z,
         typeNode: f.createArrayTypeNode(typeNode.typeArguments[0]),
         isOptional,
+        isNullable,
         jsDocTags: {},
         sourceFile,
         dependencies,
@@ -270,6 +271,7 @@ function buildZodPrimitive({
         z,
         typeNode: typeNode.typeArguments[0],
         isOptional,
+        isNullable,
         jsDocTags,
         sourceFile,
         isPartial: true,
@@ -285,6 +287,7 @@ function buildZodPrimitive({
         z,
         typeNode: typeNode.typeArguments[0],
         isOptional,
+        isNullable,
         jsDocTags,
         sourceFile,
         isRequired: true,
@@ -300,6 +303,7 @@ function buildZodPrimitive({
         z,
         typeNode: typeNode.typeArguments[0],
         isOptional,
+        isNullable,
         jsDocTags,
         sourceFile,
         dependencies,
@@ -308,6 +312,27 @@ function buildZodPrimitive({
       });
     }
 
+    // Deal with `ReadonlyArray<>` syntax
+    if (identifierName === "ReadonlyArray" && typeNode.typeArguments) {
+      return buildZodSchema(
+        z,
+        "array",
+        [
+          buildZodPrimitive({
+            z,
+            typeNode: typeNode.typeArguments[0],
+            isOptional: false,
+            jsDocTags: {},
+            sourceFile,
+            dependencies,
+            getDependencyName,
+            skipParseJSDoc,
+          }),
+        ],
+        zodProperties
+      );
+    }
+
     // Deal with `Record<>` syntax
     if (identifierName === "Record" && typeNode.typeArguments) {
       if (
@@ -345,6 +370,27 @@ function buildZodPrimitive({
       return buildZodSchema(z, "date", [], zodProperties);
     }
 
+    // Deal with `Set<>` syntax
+    if (identifierName === "Set" && typeNode.typeArguments) {
+      return buildZodSchema(
+        z,
+        "set",
+        typeNode.typeArguments.map((i) =>
+          buildZodPrimitive({
+            z,
+            typeNode: i,
+            isOptional: false,
+            jsDocTags,
+            sourceFile,
+            dependencies,
+            getDependencyName,
+            skipParseJSDoc,
+          })
+        ),
+        zodProperties
+      );
+    }
+
     // Deal with `Promise<>` syntax
     if (identifierName === "Promise" && typeNode.typeArguments) {
       return buildZodSchema(
@@ -759,11 +805,13 @@ function buildZodExtendedSchema(
     );
   }
 
-  zodCall = f.createCallExpression(
-    f.createPropertyAccessExpression(zodCall, f.createIdentifier("extend")),
-    undefined,
-    args
-  );
+  if (args?.length) {
+    zodCall = f.createCallExpression(
+      f.createPropertyAccessExpression(zodCall, f.createIdentifier("extend")),
+      undefined,
+      args
+    );
+  }
 
   return withZodProperties(zodCall, properties);
 }
@@ -836,35 +884,41 @@ function buildZodObject({
 
   let objectSchema: ts.CallExpression | undefined;
 
-  if (properties.length > 0) {
-    const parsedProperties = buildZodProperties({
-      members: properties,
-      zodImportValue: z,
-      sourceFile,
-      dependencies,
-      getDependencyName,
-      skipParseJSDoc,
-    });
+  const parsedProperties =
+    properties.length > 0
+      ? buildZodProperties({
+          members: properties,
+          zodImportValue: z,
+          sourceFile,
+          dependencies,
+          getDependencyName,
+          skipParseJSDoc,
+        })
+      : undefined;
 
-    if (schemaExtensionClauses && schemaExtensionClauses.length > 0) {
-      objectSchema = buildZodExtendedSchema(schemaExtensionClauses, [
-        f.createObjectLiteralExpression(
-          Array.from(parsedProperties.entries()).map(([key, tsCall]) => {
-            return f.createPropertyAssignment(key, tsCall);
-          }),
-          true
-        ),
-      ]);
-    } else {
-      objectSchema = buildZodSchema(z, "object", [
-        f.createObjectLiteralExpression(
-          Array.from(parsedProperties.entries()).map(([key, tsCall]) => {
-            return f.createPropertyAssignment(key, tsCall);
-          }),
-          true
-        ),
-      ]);
-    }
+  if (schemaExtensionClauses && schemaExtensionClauses.length > 0) {
+    objectSchema = buildZodExtendedSchema(
+      schemaExtensionClauses,
+      properties.length > 0
+        ? [
+            f.createObjectLiteralExpression(
+              Array.from(parsedProperties!.entries()).map(([key, tsCall]) => {
+                return f.createPropertyAssignment(key, tsCall);
+              }),
+              true
+            ),
+          ]
+        : undefined
+    );
+  } else if (properties.length > 0) {
+    objectSchema = buildZodSchema(z, "object", [
+      f.createObjectLiteralExpression(
+        Array.from(parsedProperties!.entries()).map(([key, tsCall]) => {
+          return f.createPropertyAssignment(key, tsCall);
+        }),
+        true
+      ),
+    ]);
   }
 
   if (indexSignature) {
diff --git a/src/utils/resolveModules.ts b/src/utils/resolveModules.ts
index da976e4..53caeba 100644
--- a/src/utils/resolveModules.ts
+++ b/src/utils/resolveModules.ts
@@ -138,11 +138,33 @@ const moduleToPrefix = (
         node.modifiers,
         f.createIdentifier(pascal(moduleName) + pascal(node.name.text)),
         node.typeParameters,
-        node.heritageClauses,
+        ts.visitNodes(
+          node.heritageClauses,
+          prefixInterfacesAndTypes(moduleName)
+        ),
         ts.visitNodes(node.members, prefixInterfacesAndTypes(moduleName))
       );
     }
 
+    if (ts.isHeritageClause(node)) {
+      return f.updateHeritageClause(
+        node,
+        ts.visitNodes(node.types, prefixInterfacesAndTypes(moduleName))
+      );
+    }
+
+    if (
+      ts.isExpressionWithTypeArguments(node) &&
+      ts.isIdentifier(node.expression) &&
+      (declarationNames.get(moduleName) || []).includes(node.expression.text)
+    ) {
+      return f.updateExpressionWithTypeArguments(
+        node,
+        f.createIdentifier(pascal(moduleName) + pascal(node.expression.text)),
+        node.typeArguments
+      );
+    }
+
     if (ts.isEnumDeclaration(node)) {
       return f.updateEnumDeclaration(
         node,
diff --git a/src/utils/traverseTypes.ts b/src/utils/traverseTypes.ts
new file mode 100644
index 0000000..02e0a1d
--- /dev/null
+++ b/src/utils/traverseTypes.ts
@@ -0,0 +1,71 @@
+import ts from "typescript";
+
+export type TypeNode = (
+  | ts.InterfaceDeclaration
+  | ts.TypeAliasDeclaration
+  | ts.EnumDeclaration
+) & { visited?: boolean };
+
+export function isTypeNode(node: ts.Node): node is TypeNode {
+  return (
+    ts.isInterfaceDeclaration(node) ||
+    ts.isTypeAliasDeclaration(node) ||
+    ts.isEnumDeclaration(node)
+  );
+}
+
+export function getExtractedTypeNames(
+  node: TypeNode,
+  sourceFile: ts.SourceFile,
+  typeNameMapping: Map<string, TypeNode>
+) {
+  const referenceTypeNames: string[] = [];
+
+  const recursiveExtract = (node: TypeNode) => {
+    if (node.visited) {
+      return;
+    }
+
+    const heritageClauses = (node as ts.InterfaceDeclaration).heritageClauses;
+
+    if (heritageClauses) {
+      heritageClauses.forEach((clause) => {
+        const extensionTypes = clause.types;
+        extensionTypes.forEach((extensionTypeNode) => {
+          const typeName = extensionTypeNode.expression.getText(sourceFile);
+          const typeNode = typeNameMapping.get(typeName);
+
+          referenceTypeNames.push(typeName);
+
+          if (typeNode) {
+            typeNode.visited = true;
+            recursiveExtract(typeNode);
+          }
+        });
+      });
+    }
+
+    node.forEachChild((child) => {
+      const childNode = child as ts.PropertySignature;
+      if (childNode.kind !== ts.SyntaxKind.PropertySignature) {
+        return;
+      }
+
+      if (childNode.type?.kind === ts.SyntaxKind.TypeReference) {
+        const typeNode = typeNameMapping.get(
+          childNode.type.getText(sourceFile)
+        );
+
+        referenceTypeNames.push(childNode.type.getText(sourceFile));
+
+        if (typeNode) {
+          typeNode.visited = true;
+          recursiveExtract(typeNode);
+        }
+      }
+    });
+  };
+
+  recursiveExtract(node);
+  return [node.name.text, ...referenceTypeNames];
+}
diff --git a/ts-to-zod.config.js b/ts-to-zod.config.js
index 00380ed..d9b9759 100644
--- a/ts-to-zod.config.js
+++ b/ts-to-zod.config.js
@@ -8,6 +8,7 @@ module.exports = [
     name: "example",
     input: "example/heros.ts",
     output: "example/heros.zod.ts",
+    inferredTypes: "example/heros.types.ts",
   },
   { name: "config", input: "src/config.ts", output: "src/config.zod.ts" },
 ];
diff --git a/yarn.lock b/yarn.lock
index 1d60c91..b28e2ea 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1363,6 +1363,13 @@
   resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
   integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
 
+"@cspotcode/source-map-support@^0.8.0":
+  version "0.8.1"
+  resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
+  integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
+  dependencies:
+    "@jridgewell/trace-mapping" "0.3.9"
+
 "@eslint/eslintrc@^0.4.0":
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.0.tgz#99cc0a0584d72f1df38b900fb062ba995f395547"
@@ -1584,6 +1591,14 @@
   resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec"
   integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==
 
+"@jridgewell/trace-mapping@0.3.9":
+  version "0.3.9"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
+  integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
+  dependencies:
+    "@jridgewell/resolve-uri" "^3.0.3"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+
 "@jridgewell/trace-mapping@^0.3.0":
   version "0.3.4"
   resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3"
@@ -1660,6 +1675,17 @@
     strip-ansi "^6.0.0"
     wrap-ansi "^7.0.0"
 
+"@oclif/errors@^1.3":
+  version "1.3.6"
+  resolved "https://registry.yarnpkg.com/@oclif/errors/-/errors-1.3.6.tgz#e8fe1fc12346cb77c4f274e26891964f5175f75d"
+  integrity sha512-fYaU4aDceETd89KXP+3cLyg9EHZsLD3RxF2IU9yxahhBpspWjkWi3Dy3bTgcwZ3V47BgxQaGapzJWDM33XIVDQ==
+  dependencies:
+    clean-stack "^3.0.0"
+    fs-extra "^8.1"
+    indent-string "^4.0.0"
+    strip-ansi "^6.0.1"
+    wrap-ansi "^7.0.0"
+
 "@oclif/errors@^1.3.5":
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/@oclif/errors/-/errors-1.3.5.tgz#a1e9694dbeccab10fe2fe15acb7113991bed636c"
@@ -1737,6 +1763,26 @@
   resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
   integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
 
+"@tsconfig/node10@^1.0.7":
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
+  integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==
+
+"@tsconfig/node12@^1.0.7":
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
+  integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
+
+"@tsconfig/node14@^1.0.0":
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
+  integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
+
+"@tsconfig/node16@^1.0.2":
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
+  integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
+
 "@types/async@^3.2.6":
   version "3.2.6"
   resolved "https://registry.yarnpkg.com/@types/async/-/async-3.2.6.tgz#1d49339846c6aa0b0a8a08303c21176f1b64dc4f"
@@ -2001,6 +2047,11 @@ acorn-walk@^7.1.1:
   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
   integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
 
+acorn-walk@^8.1.1:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
+  integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
+
 acorn@^7.1.1, acorn@^7.4.0:
   version "7.4.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
@@ -2011,6 +2062,11 @@ acorn@^8.2.4:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895"
   integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==
 
+acorn@^8.4.1:
+  version "8.7.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
+  integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
+
 agent-base@5:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c"
@@ -4820,7 +4876,7 @@ slice-ansi@^4.0.0:
     astral-regex "^2.0.0"
     is-fullwidth-code-point "^3.0.0"
 
-source-map-support@^0.5.17, source-map-support@^0.5.6:
+source-map-support@^0.5.6:
   version "0.5.19"
   resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
   integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
@@ -4908,6 +4964,13 @@ strip-ansi@^6.0.0:
   dependencies:
     ansi-regex "^5.0.0"
 
+strip-ansi@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
 strip-bom@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
@@ -5086,16 +5149,23 @@ tr46@^2.1.0:
   dependencies:
     punycode "^2.1.1"
 
-ts-node@^9.1.1:
-  version "9.1.1"
-  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d"
-  integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==
-  dependencies:
+ts-node@^10.9.1:
+  version "10.9.1"
+  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"
+  integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==
+  dependencies:
+    "@cspotcode/source-map-support" "^0.8.0"
+    "@tsconfig/node10" "^1.0.7"
+    "@tsconfig/node12" "^1.0.7"
+    "@tsconfig/node14" "^1.0.0"
+    "@tsconfig/node16" "^1.0.2"
+    acorn "^8.4.1"
+    acorn-walk "^8.1.1"
     arg "^4.1.0"
     create-require "^1.1.0"
     diff "^4.0.1"
     make-error "^1.1.1"
-    source-map-support "^0.5.17"
+    v8-compile-cache-lib "^3.0.1"
     yn "3.1.1"
 
 tslib@^1.8.1:
@@ -5226,6 +5296,11 @@ uuid@^3.3.2:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
+v8-compile-cache-lib@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
+  integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
+
 v8-compile-cache@^2.0.3:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 2fe8300..4f1880a 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -3,10 +3,10 @@ name: Tests
 on:
   push:
     branches:
-      - master
+      - main
   pull_request:
-    types: [ opened, synchronize, reopened ]
-    
+    types: [opened, synchronize, reopened]
+
 jobs:
   test:
     runs-on: ubuntu-latest
@@ -14,7 +14,7 @@ jobs:
       - uses: actions/checkout@v1
       - uses: actions/setup-node@v1
         with:
-          node-version: 12.16.1
+          node-version: 16.13.1
       - run: yarn
       - run: chmod +x ./bin/run
       - run: yarn test:ci
diff --git a/src/core/generate.test.ts b/src/core/generate.test.ts
index d3195d8..5a810a3 100644
--- a/src/core/generate.test.ts
+++ b/src/core/generate.test.ts
@@ -17,7 +17,7 @@ describe("generate", () => {
          **/
         email: string;
       }
-      
+
       const fly = () => console.log("I can fly!");
       `;
 
@@ -63,6 +63,7 @@ describe("generate", () => {
         export type supermanSchemaInferredType = z.infer<typeof generated.supermanSchema>;
 
         export type badassSupermanSchemaInferredType = z.infer<typeof generated.badassSupermanSchema>;
+
         expectType<spec.Name>({} as nameSchemaInferredType)
         expectType<nameSchemaInferredType>({} as spec.Name)
         expectType<spec.Superman>({} as supermanSchemaInferredType)
@@ -79,7 +80,7 @@ describe("generate", () => {
 
   describe("with enums", () => {
     const sourceText = `
-      export enum Superhero { 
+      export enum Superhero {
         Superman = "superman"
         ClarkKent = "clark-kent"
       };
@@ -125,6 +126,7 @@ describe("generate", () => {
         export type superheroSchemaInferredType = z.infer<typeof generated.superheroSchema>;
 
         export type favoriteSuperheroSchemaInferredType = z.infer<typeof generated.favoriteSuperheroSchema>;
+
         expectType<spec.Superhero>({} as superheroSchemaInferredType)
         expectType<superheroSchemaInferredType>({} as spec.Superhero)
         expectType<spec.FavoriteSuperhero>({} as favoriteSuperheroSchemaInferredType)
@@ -145,7 +147,7 @@ describe("generate", () => {
         powers: string[];
         friends: Villain[];
       }
-      
+
       export interface EvilPlan {
         owner: Villain;
         description: string;
@@ -153,27 +155,41 @@ describe("generate", () => {
       }
 
       export interface EvilPlanDetails {
-        parent: EvilPlan; // <- Unsolvable circular reference
+        parent: EvilPlan;
         steps: string[];
       }
+
+      export interface IHaveUnknownDependency {
+        dep: UnknwonDependency; // <- Missing dependency
+      }
       `;
 
     const { getZodSchemasFile, getIntegrationTestFile, errors } = generate({
       sourceText,
-      maxRun: 3,
     });
 
     it("should generate the zod schemas", () => {
       expect(getZodSchemasFile("./villain")).toMatchInlineSnapshot(`
         "// Generated by ts-to-zod
         import { z } from \\"zod\\";
-        import { Villain } from \\"./villain\\";
+        import { Villain, EvilPlan, EvilPlanDetails } from \\"./villain\\";
 
         export const villainSchema: z.ZodSchema<Villain> = z.lazy(() => z.object({
             name: z.string(),
             powers: z.array(z.string()),
             friends: z.array(villainSchema)
         }));
+
+        export const evilPlanSchema: z.ZodSchema<EvilPlan> = z.lazy(() => z.object({
+            owner: villainSchema,
+            description: z.string(),
+            details: evilPlanDetailsSchema
+        }));
+
+        export const evilPlanDetailsSchema: z.ZodSchema<EvilPlanDetails> = z.lazy(() => z.object({
+            parent: evilPlanSchema,
+            steps: z.array(z.string())
+        }));
         "
       `);
     });
@@ -193,8 +209,17 @@ describe("generate", () => {
         }
 
         export type villainSchemaInferredType = z.infer<typeof generated.villainSchema>;
+
+        export type evilPlanSchemaInferredType = z.infer<typeof generated.evilPlanSchema>;
+
+        export type evilPlanDetailsSchemaInferredType = z.infer<typeof generated.evilPlanDetailsSchema>;
+
         expectType<spec.Villain>({} as villainSchemaInferredType)
         expectType<villainSchemaInferredType>({} as spec.Villain)
+        expectType<spec.EvilPlan>({} as evilPlanSchemaInferredType)
+        expectType<evilPlanSchemaInferredType>({} as spec.EvilPlan)
+        expectType<spec.EvilPlanDetails>({} as evilPlanDetailsSchemaInferredType)
+        expectType<evilPlanDetailsSchemaInferredType>({} as spec.EvilPlanDetails)
         "
       `);
     });
@@ -202,9 +227,8 @@ describe("generate", () => {
     it("should have some errors", () => {
       expect(errors).toMatchInlineSnapshot(`
         Array [
-          "Some schemas can't be generated due to circular dependencies:
-        evilPlanSchema
-        evilPlanDetailsSchema",
+          "Some schemas can't be generated due to direct or indirect missing dependencies:
+        iHaveUnknownDependencySchema",
         ]
       `);
     });
@@ -247,6 +271,35 @@ describe("generate", () => {
     });
   });
 
+  describe("inheritance and reference type search", () => {
+    const sourceText = `
+    export type Name = "superman" | "clark kent" | "kal-l";
+    export interface Superman {
+      name: Name;
+    }`;
+
+    const { getZodSchemasFile } = generate({
+      sourceText,
+      nameFilter: (id) => id === "Superman",
+      getSchemaName: (id) => id.toLowerCase(),
+      keepComments: true,
+    });
+
+    it("should generate superman schema", () => {
+      expect(getZodSchemasFile("./hero")).toMatchInlineSnapshot(`
+        "// Generated by ts-to-zod
+        import { z } from \\"zod\\";
+
+        export const name = z.union([z.literal(\\"superman\\"), z.literal(\\"clark kent\\"), z.literal(\\"kal-l\\")]);
+
+        export const superman = z.object({
+            name: name
+        });
+        "
+      `);
+    });
+  });
+
   describe("with jsdocTags filter", () => {
     it("should generate only types with @zod", () => {
       const sourceText = `
@@ -262,7 +315,7 @@ describe("generate", () => {
 
       /**
        * Only this interface should be generated
-       * 
+       *
        * @zod
        */
       export interface Superman {
@@ -337,6 +390,7 @@ describe("generate", () => {
         export type nameSchemaInferredType = z.infer<typeof generated.nameSchema>;
 
         export type badassSupermanSchemaInferredType = z.infer<typeof generated.badassSupermanSchema>;
+
         expectType<spec.Name>({} as nameSchemaInferredType)
         expectType<nameSchemaInferredType>({} as spec.Name)
         expectType<spec.BadassSuperman>({} as badassSupermanSchemaInferredType)
@@ -350,10 +404,10 @@ describe("generate", () => {
     const sourceText = `
       export namespace Metropolis {
         export type Name = "superman" | "clark kent" | "kal-l";
-  
+
         // Note that the Superman is declared after
         export type BadassSuperman = Omit<Superman, "underKryptonite">;
-  
+
         export interface Superman {
           name: Name;
           age: number;
@@ -363,7 +417,7 @@ describe("generate", () => {
            **/
           email: string;
         }
-        
+
         const fly = () => console.log("I can fly!");
       }
       `;
@@ -410,6 +464,7 @@ describe("generate", () => {
         export type metropolisSupermanSchemaInferredType = z.infer<typeof generated.metropolisSupermanSchema>;
 
         export type metropolisBadassSupermanSchemaInferredType = z.infer<typeof generated.metropolisBadassSupermanSchema>;
+
         expectType<spec.MetropolisName>({} as metropolisNameSchemaInferredType)
         expectType<metropolisNameSchemaInferredType>({} as spec.MetropolisName)
         expectType<spec.MetropolisSuperman>({} as metropolisSupermanSchemaInferredType)
diff --git a/src/core/generateZodSchema.test.ts b/src/core/generateZodSchema.test.ts
index 6e1936a..ee6331c 100644
--- a/src/core/generateZodSchema.test.ts
+++ b/src/core/generateZodSchema.test.ts
@@ -109,7 +109,7 @@ describe("generateZodSchema", () => {
   });
 
   it("should generate a nativeEnum schema", () => {
-    const source = `export enum Superhero = { 
+    const source = `export enum Superhero = {
       Superman = "superman",
       ClarkKent = "clark_kent",
     };`;
@@ -221,6 +221,13 @@ describe("generateZodSchema", () => {
     );
   });
 
+  it("should generate a set schema", () => {
+    const source = `export type EnemiesPowers = Set<string>;`;
+    expect(generate(source)).toMatchInlineSnapshot(
+      `"export const enemiesPowersSchema = z.set(z.string());"`
+    );
+  });
+
   it("should generate a function schema", () => {
     const source = `export type KillSuperman = (withKryptonite: boolean, method: string) => Promise<boolean>;`;
     expect(generate(source)).toMatchInlineSnapshot(
@@ -298,6 +305,13 @@ describe("generateZodSchema", () => {
     );
   });
 
+  it("should fallback on Array for ReadonlyArray<T>", () => {
+    const source = `export type ReadonlySupermen = ReadonlyArray<Superman>;`;
+    expect(generate(source)).toMatchInlineSnapshot(
+      `"export const readonlySupermenSchema = z.array(supermanSchema);"`
+    );
+  });
+
   it("should generate a partial schema", () => {
     const source = `export type SupermanUnderKryptonite = Partial<Hero>;`;
     expect(generate(source)).toMatchInlineSnapshot(
@@ -370,6 +384,13 @@ describe("generateZodSchema", () => {
     `);
   });
 
+  it("should generate an variable assignment if an extending type has no new fields", () => {
+    const source = "export interface Superman extends Clark {}";
+    expect(generate(source)).toMatchInlineSnapshot(
+      `"export const supermanSchema = clarkSchema;"`
+    );
+  });
+
   it("should generate a merged schema when two extends are used", () => {
     const source = `export interface Superman extends Clark extends KalL {
         withPower: boolean;
@@ -435,7 +456,7 @@ describe("generateZodSchema", () => {
 
   it("should deal with index access type (array item)", () => {
     const source = `export type SupermanPower = Superman["powers"][-1];
-    
+
     export type Superman = {
       powers: Array<Power>
     };`;
@@ -447,7 +468,7 @@ describe("generateZodSchema", () => {
 
   it("should deal with index access type (array item bis)", () => {
     const source = `export type SupermanPower = Superman["powers"][-1];
-    
+
     export type Superman = {
       powers: Power[]
     };`;
@@ -459,7 +480,7 @@ describe("generateZodSchema", () => {
 
   it("should deal with index access type (record item)", () => {
     const source = `export type SupermanPower = Superman["powers"][-1];
-    
+
     export type Superman = {
       powers: Record<string, Power>
     };`;
@@ -471,7 +492,7 @@ describe("generateZodSchema", () => {
 
   it("should deal with index access type (record item) (interface)", () => {
     const source = `export type SupermanPower = Superman["powers"][-1];
-    
+
     export interface Superman {
       powers: Record<string, Power>
     };`;
@@ -483,7 +504,7 @@ describe("generateZodSchema", () => {
 
   it("should deal with index access type (tuple)", () => {
     const source = `export type SupermanPower = Superman["powers"][1];
-    
+
     export type Superman = {
       powers: ["fly", "burnStuff"]
     };`;
@@ -496,7 +517,7 @@ describe("generateZodSchema", () => {
   // TODO
   it.skip("should deal with index access type (nested array item)", () => {
     const source = `export type SupermanPower = Superman["powers"][-1][-1];
-    
+
     export type Superman = {
       powers: Power[][]
     };`;
@@ -508,7 +529,7 @@ describe("generateZodSchema", () => {
 
   it("should deal with index access type (inline array item)", () => {
     const source = `export type SupermanPower = Superman["powers"][-1];
-    
+
     export type Superman = {
       powers: Array<{type: string}>
     };`;
@@ -520,7 +541,7 @@ describe("generateZodSchema", () => {
 
   it("should deal with index access type (inline array item bis)", () => {
     const source = `export type SupermanPower = Superman["powers"][-1];
-    
+
     export type Superman = {
       powers: {type: string}[]
     };`;
@@ -532,7 +553,7 @@ describe("generateZodSchema", () => {
 
   it("should deal with index access type (inline record)", () => {
     const source = `export type SupermanPower = Superman["powers"][-1];
-    
+
     export type Superman = {
       powers: Record<string, {type: string}>
     };`;
@@ -607,7 +628,7 @@ describe("generateZodSchema", () => {
        * @format email
        */
       email: string;
-    
+
       /**
        * The name of the hero.
        *
@@ -615,24 +636,24 @@ describe("generateZodSchema", () => {
        * @maxLength 50
        */
       name: string;
-    
+
       /**
        * The phone number of the hero.
        *
        * @pattern ^([+]?d{1,2}[-s]?|)d{3}[-s]?d{3}[-s]?d{4}$
        */
       phoneNumber: string;
-    
+
       /**
        * Does the hero has super power?
        *
        * @default true
        */
       hasSuperPower?: boolean;
-      
+
       /**
        * The age of the hero
-       * 
+       *
        * @minimum 0
        * @maximum 500
        */
@@ -691,7 +712,7 @@ describe("generateZodSchema", () => {
        * @format email, "Should be an email"
        */
       enemyEmail: string;
-      
+
       /**
        * The email of the superman.
        *
@@ -731,7 +752,7 @@ describe("generateZodSchema", () => {
        * @format email should be an email
        */
       email: string;
-    
+
       /**
        * The name of the hero.
        *
@@ -739,10 +760,10 @@ describe("generateZodSchema", () => {
        * @maxLength 50 should be less than 50
        */
       name: string;
-    
+
       /**
        * The age of the hero
-       * 
+       *
        * @minimum 0 you are too young
        * @maximum 500, "you are too old"
        */
@@ -824,6 +845,56 @@ describe("generateZodSchema", () => {
     `);
   });
 
+  it("should deal with array of null or null", () => {
+    const source = `export type Example = {
+        field?: Array<string | null> | null
+    }`;
+
+    expect(generate(source)).toMatchInlineSnapshot(`
+      "export const exampleSchema = z.object({
+          field: z.array(z.string().nullable()).optional().nullable()
+      });"
+    `);
+  });
+
+  it("should deal with partial or null", () => {
+    const source = `export type Example = {
+        field: Partial<{foo: string}> | null
+    }`;
+
+    expect(generate(source)).toMatchInlineSnapshot(`
+      "export const exampleSchema = z.object({
+          field: z.object({
+              foo: z.string()
+          }).nullable().partial()
+      });"
+    `);
+  });
+
+  it("should deal with ReadonlyArray or null", () => {
+    const source = `export type Example = {
+        field: ReadonlyArray<"foo" | "bar"> | null
+    }`;
+
+    expect(generate(source)).toMatchInlineSnapshot(`
+      "export const exampleSchema = z.object({
+          field: z.array(z.union([z.literal(\\"foo\\"), z.literal(\\"bar\\")])).nullable()
+      });"
+    `);
+  });
+
+  it("should deal with Record or null", () => {
+    const source = `export type Example = {
+        field: Record<string, string> | null
+    }`;
+
+    expect(generate(source)).toMatchInlineSnapshot(`
+      "export const exampleSchema = z.object({
+          field: z.record(z.string()).nullable()
+      });"
+    `);
+  });
+
   it("should allow nullable on union properties", () => {
     const source = `export interface A {
       a: number | string | null;
diff --git a/src/core/validateGeneratedTypes.test.ts b/src/core/validateGeneratedTypes.test.ts
index bcf8755..3d8925d 100644
--- a/src/core/validateGeneratedTypes.test.ts
+++ b/src/core/validateGeneratedTypes.test.ts
@@ -20,10 +20,10 @@ describe("validateGeneratedTypes", () => {
     const integrationTests = {
       sourceText: `// Generated by ts-to-zod
       import { z } from "zod";
-      
+
       import * as spec from "./${sourceTypes.relativePath.slice(0, -3)}";
       import * as generated from "./${zodSchemas.relativePath.slice(0, -3)}";
-      
+
       // eslint-disable-next-line @typescript-eslint/no-unused-vars
       function expectType<T>(_: T) {
         /* noop */
@@ -66,10 +66,10 @@ describe("validateGeneratedTypes", () => {
     const integrationTests = {
       sourceText: `// Generated by ts-to-zod
         import { z } from "zod";
-        
+
         import * as spec from "./${sourceTypes.relativePath.slice(0, -3)}";
         import * as generated from "./${zodSchemas.relativePath.slice(0, -3)}";
-        
+
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
         function expectType<T>(_: T) {
           /* noop */
@@ -126,10 +126,10 @@ describe("validateGeneratedTypes", () => {
     const integrationTests = {
       sourceText: `// Generated by ts-to-zod
       import { z } from "zod";
-      
+
       import * as spec from "./${sourceTypes.relativePath.slice(0, -3)}";
       import * as generated from "./${zodSchemas.relativePath.slice(0, -3)}";
-      
+
       // eslint-disable-next-line @typescript-eslint/no-unused-vars
       function expectType<T>(_: T) {
         /* noop */
@@ -179,10 +179,10 @@ describe("validateGeneratedTypes", () => {
     const integrationTests = {
       sourceText: `// Generated by ts-to-zod
       import { z } from "zod";
-      
+
       import * as spec from "./${sourceTypes.relativePath.slice(0, -3)}";
       import * as generated from "./${zodSchemas.relativePath.slice(0, -3)}";
-      
+
       // eslint-disable-next-line @typescript-eslint/no-unused-vars
       function expectType<T>(_: T) {
         /* noop */
diff --git a/src/utils/resolveModules.test.ts b/src/utils/resolveModules.test.ts
index bdb2875..4f4ad74 100644
--- a/src/utils/resolveModules.test.ts
+++ b/src/utils/resolveModules.test.ts
@@ -32,7 +32,7 @@ describe("resolveModules", () => {
 
   it("should prefix enum", () => {
     const sourceText = `export namespace Metropolis {
-      export enum Superhero { 
+      export enum Superhero {
         Superman = "superman",
         ClarkKent = "clark_kent",
       };
@@ -63,13 +63,21 @@ describe("resolveModules", () => {
         hasPower: boolean;
         weakness: Weakness;
       }
-      
+
+      export interface Clark {
+        hasGlasses: boolean
+      }
+
       export type SupermanBis = {
         fullName: Name;
         name: { first: Name; last: Name };
         hasPower: boolean;
         weakness: Weakness;
       }
+
+      export interface ExtendedSuperman extends Superman, Clark {
+        hasEvenMorePower: boolean;
+      }
     }`;
 
     expect(print(resolveModules(sourceText))).toMatchInlineSnapshot(`
@@ -85,6 +93,9 @@ describe("resolveModules", () => {
           hasPower: boolean;
           weakness: Weakness;
       }
+      export interface MetropolisClark {
+          hasGlasses: boolean;
+      }
       export type MetropolisSupermanBis = {
           fullName: MetropolisName;
           name: {
@@ -94,6 +105,9 @@ describe("resolveModules", () => {
           hasPower: boolean;
           weakness: Weakness;
       };
+      export interface MetropolisExtendedSuperman extends MetropolisSuperman, MetropolisClark {
+          hasEvenMorePower: boolean;
+      }
       "
     `);
   });