Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TSC generates wrong typedefs from zod #1986

Closed
18ivan18 opened this issue Feb 4, 2023 · 1 comment
Closed

TSC generates wrong typedefs from zod #1986

18ivan18 opened this issue Feb 4, 2023 · 1 comment

Comments

@18ivan18
Copy link

18ivan18 commented Feb 4, 2023

The other day I needed to use a zod schema which defined either all of the properties in an object or none of them. Basically something like

const ExampleSchema = z.object(
  { openingHours: z.date(), closingHours: z.date() }
);
type Example = z.infer<typeof ExampleSchema>;

// we want this to be correct
const example: Example = { 
    openingHours: new Date(),
    closingHours: new Date(),
};

// we want this to be correct
const example: Example = {};

// we want this to be incorrect
const example: Example = { 
    closingHours: new Date(),
};

Just creating a schema like that was not enough for me, so I jumped ahead and created a factory that generates such schemas based on the object with required properties. The code turned out to be pretty short and easy, the type definitions were correct, I was happy.

export function defineAllOrNothingSchema<T extends Record<string, ZodType>>(
  objectWithAllProperties: T
) {
  const allProperties = z.object(objectWithAllProperties);
  const objectWithNoneOfTheProperties = Object.keys(
    objectWithAllProperties
  ).reduce(
    (prev, key) => ({ ...prev, [key]: z.undefined() }),
    {} as {
      [Key in keyof T]: z.ZodUndefined;
    }
  );
  const noneOfTheProperties = z.object(objectWithNoneOfTheProperties);

  return allProperties.or(noneOfTheProperties);
}

const ExampleSchema = defineAllOrNothingSchema(
  { openingHours: z.date(), closingHours: z.date() }
);

type Example = z.infer<typeof ExampleSchema>;

// correct
const example: Example = { 
    openingHours: new Date(),
    closingHours: new Date(),
};

// correct
const example: Example = {};

// incorrect
const example: Example = { 
    closingHours: new Date(),
};

The problem comes when I tried to compile it with tsc.

tsconfig.json
{
  "include": ["./define-all-or-nothing-schema.ts"],
  "compilerOptions": {
    "noUncheckedIndexedAccess": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "strict": true,
    "target": "es2021",
    "sourceMap": false,
    "lib": ["es2017"],
    "removeComments": false,
    "composite": true,
    "incremental": true,
    "module": "commonjs",
    "declaration": true,
    "outDir": "./lib/cjs",
    "declarationDir": "./lib/typedefs/"
  },
}

lib/typedefs/define-all-or-nothing-schema.d.ts

import { z, ZodType } from "zod";
export declare function defineAllOrNothingSchema<
  T extends Record<string, ZodType>
>(
  objectWithAllProperties: T
): z.ZodUnion<
  [
    z.ZodObject<
      T,
      "strip",
      z.ZodTypeAny,
      z.objectUtil.addQuestionMarks<{
        [k_2 in keyof T]: T[k_2]["_output"];
      }> extends infer T_1
        ? {
            [k_1 in keyof T_1]: z.objectUtil.addQuestionMarks<{
              [k in keyof T]: T[k]["_output"];
            }>[k_1];
          }
        : never,
      z.objectUtil.addQuestionMarks<{
        [k_2_1 in keyof T]: T[k_2_1]["_input"];
      }> extends infer T_2
        ? {
            [k_3 in keyof T_2]: z.objectUtil.addQuestionMarks<{
              [k_2 in keyof T]: T[k_2]["_input"];
            }>[k_3];
          }
        : never
    >,
    z.ZodObject<
      { [Key in keyof T]: z.ZodUndefined },
      "strip",
      z.ZodTypeAny,
      z.objectUtil.addQuestionMarks<
        {
          [Key in keyof T]: z.ZodUndefined;
        } extends infer T_5 extends z.ZodRawShape
          ? {
              [k_5 in keyof T_5]: {
                [Key in keyof T]: z.ZodUndefined;
              }[k_5]["_output"];
            }
          : never
      > extends infer T_3
        ? {
            [k_1_1 in keyof T_3]: z.objectUtil.addQuestionMarks<
              {
                [Key in keyof T]: z.ZodUndefined;
              } extends infer T_4 extends z.ZodRawShape
                ? {
                    [k_4 in keyof T_4]: {
                      [Key in keyof T]: z.ZodUndefined;
                    }[k_4]["_output"];
                  }
                : never
            >[k_1_1];
          }
        : never,
      z.objectUtil.addQuestionMarks<
        {
          [Key in keyof T]: z.ZodUndefined;
        } extends infer T_8 extends z.ZodRawShape
          ? {
              [k_2_3 in keyof T_8]: {
                [Key in keyof T]: z.ZodUndefined;
              }[k_2_3]["_input"];
            }
          : never
      > extends infer T_6
        ? {
            [k_3_1 in keyof T_6]: z.objectUtil.addQuestionMarks<
              {
                [Key in keyof T]: z.ZodUndefined;
              } extends infer T_7 extends z.ZodRawShape
                ? {
                    [k_2_2 in keyof T_7]: {
                      [Key in keyof T]: z.ZodUndefined;
                    }[k_2_2]["_input"];
                  }
                : never
            >[k_3_1];
          }
        : never
    >
  ]
>;
export declare function defineSchemaWithRequiredTuplesOfProperties<
  T extends Record<string, ZodType>,
  U extends Record<string, ZodType>
>(
  allOrNothingProperties: T,
  otherProperties: U
): z.ZodIntersection<
  z.ZodObject<
    U,
    "strip",
    z.ZodTypeAny,
    z.objectUtil.addQuestionMarks<{
      [k_2 in keyof U]: U[k_2]["_output"];
    }> extends infer T_1
      ? {
          [k_1 in keyof T_1]: z.objectUtil.addQuestionMarks<{
            [k in keyof U]: U[k]["_output"];
          }>[k_1];
        }
      : never,
    z.objectUtil.addQuestionMarks<{
      [k_2_1 in keyof U]: U[k_2_1]["_input"];
    }> extends infer T_2
      ? {
          [k_3 in keyof T_2]: z.objectUtil.addQuestionMarks<{
            [k_2 in keyof U]: U[k_2]["_input"];
          }>[k_3];
        }
      : never
  >,
  z.ZodUnion<
    [
      z.ZodObject<
        T,
        "strip",
        z.ZodTypeAny,
        z.objectUtil.addQuestionMarks<{
          [k_5 in keyof T]: T[k_5]["_output"];
        }> extends infer T_3
          ? {
              [k_1_1 in keyof T_3]: z.objectUtil.addQuestionMarks<{
                [k_4 in keyof T]: T[k_4]["_output"];
              }>[k_1_1];
            }
          : never,
        z.objectUtil.addQuestionMarks<{
          [k_2_3 in keyof T]: T[k_2_3]["_input"];
        }> extends infer T_4
          ? {
              [k_3_1 in keyof T_4]: z.objectUtil.addQuestionMarks<{
                [k_2_2 in keyof T]: T[k_2_2]["_input"];
              }>[k_3_1];
            }
          : never
      >,
      z.ZodObject<
        { [Key in keyof T]: z.ZodUndefined },
        "strip",
        z.ZodTypeAny,
        z.objectUtil.addQuestionMarks<
          {
            [Key in keyof T]: z.ZodUndefined;
          } extends infer T_7 extends z.ZodRawShape
            ? {
                [k_7 in keyof T_7]: {
                  [Key in keyof T]: z.ZodUndefined;
                }[k_7]["_output"];
              }
            : never
        > extends infer T_5
          ? {
              [k_1_2 in keyof T_5]: z.objectUtil.addQuestionMarks<
                {
                  [Key in keyof T]: z.ZodUndefined;
                } extends infer T_6 extends z.ZodRawShape
                  ? {
                      [k_6 in keyof T_6]: {
                        [Key in keyof T]: z.ZodUndefined;
                      }[k_6]["_output"];
                    }
                  : never
              >[k_1_2];
            }
          : never,
        z.objectUtil.addQuestionMarks<
          {
            [Key in keyof T]: z.ZodUndefined;
          } extends infer T_10 extends z.ZodRawShape
            ? {
                [k_2_5 in keyof T_10]: {
                  [Key in keyof T]: z.ZodUndefined;
                }[k_2_5]["_input"];
              }
            : never
        > extends infer T_8
          ? {
              [k_3_2 in keyof T_8]: z.objectUtil.addQuestionMarks<
                {
                  [Key in keyof T]: z.ZodUndefined;
                } extends infer T_9 extends z.ZodRawShape
                  ? {
                      [k_2_4 in keyof T_9]: {
                        [Key in keyof T]: z.ZodUndefined;
                      }[k_2_4]["_input"];
                    }
                  : never
              >[k_3_2];
            }
          : never
      >
    ]
  >
>;

and the errors:

Type 'k_1' cannot be used to index type 'addQuestionMarks<{ [k in keyof T]: T[k]["_output"]; }>'. at line 17

Type 'k_3' cannot be used to index type 'addQuestionMarks<{ [k_2 in keyof T]: T[k_2]["_input"]; }>'. line 26
and so on.

The generated type uses zod's complex types and after being compiled they seem incorrect and full of error messages. I took a look myself and began to understand why they are wrong but can't seem to work out why they are generated in such a way that they are now wrong although no indication of errors is present in the code. Maybe it's some incompatibility between my tsconfig and zod? I also setup a minimal working example in a git repo so you can reproduce the problem and check out everything in piece and quiet. Here's the repo: https://github.com/18ivan18/zod-create-all-or-nothing-schema.

Every piece of advice is welcome.

@JacobWeisenburger
Copy link
Contributor

JacobWeisenburger commented Feb 4, 2023

Is this what you are looking for?

const ExampleSchema = z.object( {
    openingHours: z.date(),
    closingHours: z.date(),
} ).or( z.record( z.string(), z.never() ) )
type Example = z.infer<typeof ExampleSchema>
// type Example = {
//     openingHours: Date
//     closingHours: Date
// } | Record<string, never>

const example1: Example = {
    openingHours: new Date(),
    closingHours: new Date(),
}
console.log( ExampleSchema.safeParse( example1 ).success ) // true

const example2: Example = {}
console.log( ExampleSchema.safeParse( example2 ).success ) // true

// compile error as expected
//    vvvvvvvv
const example3: Example = {
    closingHours: new Date(),
}
console.log( ExampleSchema.safeParse( example3 ).success ) // false

If you found my answer satisfactory, please consider supporting me. Even a small amount is greatly appreciated. Thanks friend! 🙏
https://github.com/sponsors/JacobWeisenburger

Repository owner locked and limited conversation to collaborators Feb 4, 2023
@JacobWeisenburger JacobWeisenburger converted this issue into discussion #1988 Feb 4, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants