zod icon indicating copy to clipboard operation
zod copied to clipboard

Addition of `.keyof()` breaks assignability for `ZodObjects`

Open itsgiacoliketaco opened this issue 3 years ago • 2 comments

Hi there,

#1216 appears to break assignability of ZodObjects in a subtle way. For example, the following worked before Zod 3.17.9, but fails in the latest Zod (3.17.10). I'm on TypeScript version 4.7.4.

declare const superset: { foo: string } & z.ZodObject<{
  prop1: z.ZodString;
  prop2: z.ZodNumber;
}>; // The `{ foo: string }` intersection is necessary for this to fail for some reason.

const subset: z.ZodObject<{ prop1: z.ZodString }> = superset;
//    ^^^^^^ Error here

This produces the following type error:

Type '{ foo: string; } & ZodObject<{ prop1: ZodString; prop2: ZodNumber; }, "strip", ZodTypeAny, { prop1: string; prop2: number; }, { prop1: string; prop2: number; }>' is not assignable to type 'ZodObject<{ prop1: ZodString; }, "strip", ZodTypeAny, { prop1: string; }, { prop1: string; }>'.
  The types returned by 'keyof()._parse(...)' are incompatible between these types.
    Type 'ParseReturnType<"prop1" | "prop2">' is not assignable to type 'ParseReturnType<"prop1">'.
      Type 'OK<"prop1" | "prop2">' is not assignable to type 'ParseReturnType<"prop1">'.
        Type 'OK<"prop1" | "prop2">' is not assignable to type 'OK<"prop1">'.
          Type '"prop1" | "prop2"' is not assignable to type '"prop1"'.
            Type '"prop2"' is not assignable to type '"prop1"'.ts(2322)

The problem arises because z.ZodEnum<["prop1", "prop2"]> is not assignable to z.ZodEnum<["prop1"]>. This makes sense! However, I would argue that superset should be assignable to subset, because that aligns with TypeScript's structural typing. In particular, in TypeScript,

interface Superset {
  prop1: string;
  prop2: number;
}

is assignable to

interface Subset {
  prop1: string
}

As I mentioned in the comment, for some reason this error doesn't surface unless you intersect the z.ZodObject with a non-empty object type (i.e., { foo: string } in the example above). As far as I can tell, this is a TypeScript quirk. I think with the addition of .keyof(), TypeScript should be raising an error even without that intersection present. Maybe I'm missing something?

Not sure what the solution is here other than removing .keyof() I guess. It could be replaced with z.keyOf(object: z.ZodObject) or something else that doesn't change the ZodObject type? Or you could decide that this problem doesn't matter! But a project I'm working on does lots of fun stuff with Zod and we rely on ZodObject assignability working this way 🥲.

Thanks for reading and for the fantastic library.

P.S. I feel like somehow the new variance annotations could be used to solve this but I'm not sure how.

itsgiacoliketaco avatar Jul 26 '22 17:07 itsgiacoliketaco

So if i understand correctly, the addition of keyof made superset assignment to a subset impossible.
Since i made the implementation of keyof, i can try to create a separate helper like suggested. z.keyOf() and remove the method from ZodObject. @colinhacks what do you think ? this will be a breaking change, but i guess the keyof feature is not that much used ATM since it's new.

ecyrbe avatar Aug 10 '22 10:08 ecyrbe

if i understand correctly, the addition of keyof made superset assignment to a subset impossible.

Exactly! Thanks for considering this.

itsgiacoliketaco avatar Aug 12 '22 00:08 itsgiacoliketaco

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 Oct 11 '22 05:10 stale[bot]

Not stale, still an issue that prevents us from updating zod

mayorandrew avatar Oct 11 '22 07:10 mayorandrew

I suppose comments don't count as activity? This issue continues to prevent us from updating zod, so I wouldn't consider it stale.

itsgiacoliketaco avatar Oct 18 '22 18:10 itsgiacoliketaco