zod icon indicating copy to clipboard operation
zod copied to clipboard

ZodObject.merge() looses base shape type when schema is created via a function

Open callumgare opened this issue 2 years ago • 3 comments

Given two schemas like:

const baseSchema = z.object({
  foo: z.string()
});

const extentionSchema = z.object({
  bar: z.string()
});

If they are merged together like so:

const mergedSchema = baseSchema.merge(extentionSchema);

The resulting schema should produce a type that combines both existing schema, which it does: Screen Shot 2022-10-03 at 16 04 42

However if a factory pattern is used to construct that merged schema like so:

function createSchema<
  BaseSchema extends z.ZodObject<ZodRawShape>,
  ExtentionSchema extends z.ZodObject<ZodRawShape>
>(baseSchema: BaseSchema, extentionSchema: ExtentionSchema) {
  return baseSchema.merge(extentionSchema);
}

And a new schema is created using that function:

const mergedSchema = createSchema(baseSchema, extentionSchema);

Then the type of resulting schema extends an any object which is not very helpful: Screen Shot 2022-10-03 at 16 08 49

I've created a live example here and you can observe the constructed types by hovering over objectBasedOnMergedSchema on line 22 and 31: https://codesandbox.io/s/zod-merge-typescript-issue-ohz657?file=/src/index.ts

callumgare avatar Oct 03 '22 05:10 callumgare

Try something like this

/************ Creating Schema via factory function ************/
(() => {
  function createSchema<
    BaseSchema extends z.AnyZodObject,
    ExtentionSchema extends z.AnyZodObject
  >(
    baseSchema: BaseSchema,
    extentionSchema: ExtentionSchema
  ): z.ZodObject<
    z.extendShape<BaseSchema, ReturnType<ExtentionSchema["_def"]["shape"]>>,
    ExtentionSchema["_def"]["unknownKeys"],
    ExtentionSchema["_def"]["catchall"]
  > {
    return baseSchema.merge(extentionSchema);
  }
  const mergedSchema = createSchema(baseSchema, extentionSchema);
  // This should fail but it doesn't
  const objectBasedOnMergedSchema: z.infer<typeof mergedSchema> = {
    bar: 42
  };
})(); // IIFE is just to scope variables

See here for more info

elmeister avatar Oct 03 '22 15:10 elmeister

Good to know about AnyZodObject! Ta. Sadly that function is just a significantly stripped down version of the one I'm actually using which not only merges two schema but allows you to specify which properties you wish to pick from the schema to merge. I think it might be too complex to explicitly provide a return type for which is why I was hoping whatever the issue is with the return type of a merged object inside a factory function could be improved so I can use make use of the implicit return type. My actual function is:

export type ObjectFromList<T extends ReadonlyArray<string>, V = boolean> = {
  [K in (T extends ReadonlyArray<infer U> ? U : never)]: V
};

export function createSchemaFromSuperset<
  BaseSchema extends z.ZodObject<any>,
  CommonSchema extends z.ZodObject<any>,
  CommonProps extends Exclude<keyof z.infer<CommonSchema>, number | symbol>,
> (
  baseSchema: BaseSchema,
  commonSchema: CommonSchema,
  requiredProperties: ReadonlyArray<CommonProps>,
  optionalProperties: ReadonlyArray<CommonProps> = []
) {
  type RequiredPropertiesObj = ObjectFromList<typeof requiredProperties, true>
  const requiredPropertiesObj: RequiredPropertiesObj = requiredProperties.reduce((r, key): RequiredPropertiesObj => {
    return {
      ...r,
      [key]: true
    }
  }, {} as RequiredPropertiesObj)

  type OptionalPropertiesObj = ObjectFromList<typeof optionalProperties, true>
  const optionalPropertiesObj: OptionalPropertiesObj = optionalProperties.reduce((r, key): OptionalPropertiesObj => {
    return {
      ...r,
      [key]: true
    }
  }, {} as OptionalPropertiesObj)

  return baseSchema.merge(
    commonSchema.pick(requiredPropertiesObj).merge(
      commonSchema.pick(optionalPropertiesObj).partial(optionalPropertiesObj)
    )
  )
}

callumgare avatar Oct 03 '22 15:10 callumgare

Hey 👋

Same idea as the other issue I've replied to..

  function createSchema<Shape0 extends ZodRawShape, Shape1 extends ZodRawShape>(
    baseSchema: ZodObject<Shape0>,
    extentionSchema: ZodObject<Shape1>
  ) {
    return baseSchema.merge(extentionSchema);
  }

  const mergedSchema = createSchema(baseSchema, extentionSchema);

  const objectBasedOnMergedSchema: z.infer<typeof mergedSchema> = {
    bar: 42, // ts error.
  };

Let me know if that worked for you.

igalklebanov avatar Dec 16 '22 18:12 igalklebanov

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Mar 17 '23 00:03 stale[bot]