Repository: fabien0102/ts-to-zod (ts)

Issue:
[Bug] Validation fails when using `undefined` primitive in non-optional property

## Input
```ts
export type User = {
  username: string | undefined
}
```

## Expected output
```ts
// Generated by ts-to-zod
import { z } from "zod";

export const userSchema = z.object({
  username: z.union([z.string(), z.undefined()]),
});
```

## Actual output

``` 
✖ Validating generated types
Error: 'userSchema' is not compatible with 'User':
Argument of type '{ username?: string | undefined; }' is not assignable to parameter of type 'User'.
  Property 'username' is optional in type '{ username?: string | undefined; }' but required in type 'User'.
```

_Originally posted by @johnnyp-gg in https://github.com/fabien0102/ts-to-zod/issues/203#issuecomment-2104798136_
            

Pull request description:
fix: handle `undefined` properties as optional during validation phase
# Why

Fixes #239 

During validation, we apply the same behaviour as #140 and #203


Patch:
diff --git a/src/utils/fixOptionalAny.ts b/src/utils/fixOptionalAny.ts
index fca8f47..7367835 100644
--- a/src/utils/fixOptionalAny.ts
+++ b/src/utils/fixOptionalAny.ts
@@ -1,11 +1,12 @@
 import ts, { factory as f } from "typescript";
 
 /**
- * Add optional property to `any` or imported types to workaround comparison issue.
+ * Add optional property to `any`, `undefined` or imported types to workaround comparison issue.
  *
  * ref:
  * -> https://github.com/fabien0102/ts-to-zod/issues/140
  * -> https://github.com/fabien0102/ts-to-zod/issues/203
+ * -> https://github.com/fabien0102/ts-to-zod/issues/239
  *
  */
 export function fixOptionalAny(
@@ -14,8 +15,12 @@ export function fixOptionalAny(
 ) {
   function shouldAddQuestionToken(node: ts.TypeNode) {
     return (
+      // https://github.com/fabien0102/ts-to-zod/issues/140
       node.kind === ts.SyntaxKind.AnyKeyword ||
+      // https://github.com/fabien0102/ts-to-zod/issues/239
+      node.kind === ts.SyntaxKind.UndefinedKeyword ||
       // Handling type referencing imported types
+      // https://github.com/fabien0102/ts-to-zod/issues/203
       (ts.isTypeReferenceNode(node) &&
         importsToHandleAsAny.has(node.typeName.getText(sourceFile)))
     );


Test patch:
diff --git a/src/core/validateGeneratedTypes.test.ts b/src/core/validateGeneratedTypes.test.ts
index a53bfdc..71f9170 100644
--- a/src/core/validateGeneratedTypes.test.ts
+++ b/src/core/validateGeneratedTypes.test.ts
@@ -922,6 +922,106 @@ describe("validateGeneratedTypes", () => {
     expect(errors).toEqual([]);
   });
 
+  it("should return no error if we use a non-optional undefined", () => {
+    const sourceTypes = {
+      sourceText: `
+      export interface Citizen {
+        villain: undefined;
+      };
+    `,
+      relativePath: "source.ts",
+    };
+
+    const zodSchemas = {
+      sourceText: `// Generated by ts-to-zod
+    import { z } from "zod";
+    export const citizenSchema = z.object({
+      villain: z.undefined()
+    });
+    `,
+      relativePath: "source.zod.ts",
+    };
+
+    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 */
+      }
+
+      export type CitizenInferredType = z.infer<typeof generated.citizenSchema>;
+
+      expectType<CitizenInferredType>({} as spec.Citizen);
+      expectType<spec.Citizen>({} as CitizenInferredType);
+  `,
+      relativePath: "source.integration.ts",
+    };
+
+    const errors = validateGeneratedTypes({
+      sourceTypes,
+      zodSchemas,
+      integrationTests,
+      skipParseJSDoc: false,
+    });
+
+    expect(errors).toEqual([]);
+  });
+
+  it("should return no error if we use a non-optional undefined in union type", () => {
+    const sourceTypes = {
+      sourceText: `
+      export interface Citizen {
+        villain: string | undefined;
+      };
+    `,
+      relativePath: "source.ts",
+    };
+
+    const zodSchemas = {
+      sourceText: `// Generated by ts-to-zod
+    import { z } from "zod";
+    export const citizenSchema = z.object({
+      villain: z.union([z.string(), z.undefined()])
+    });
+    `,
+      relativePath: "source.zod.ts",
+    };
+
+    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 */
+      }
+
+      export type CitizenInferredType = z.infer<typeof generated.citizenSchema>;
+
+      expectType<CitizenInferredType>({} as spec.Citizen);
+      expectType<spec.Citizen>({} as CitizenInferredType);
+  `,
+      relativePath: "source.integration.ts",
+    };
+
+    const errors = validateGeneratedTypes({
+      sourceTypes,
+      zodSchemas,
+      integrationTests,
+      skipParseJSDoc: false,
+    });
+
+    expect(errors).toEqual([]);
+  });
+
   it("should return an error if the types doesn't match", () => {
     const sourceTypes = {
       sourceText: `