Skip to content
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

Exporting schemas from package breaks instanceof #2241

Open
ahmafi opened this issue Mar 25, 2023 · 13 comments
Open

Exporting schemas from package breaks instanceof #2241

ahmafi opened this issue Mar 25, 2023 · 13 comments

Comments

@ahmafi
Copy link

ahmafi commented Mar 25, 2023

When I export a schema from a package and use that schema in another package, the instanceof operator doesn't work on ZodError or Schema. I'm also new to creating packages, so maybe there is something that I miss there!

How I'm using the exported schemas:

import { mySchema } from 'mylib';
import { z } from 'zod';

console.log(mySchema instanceof z.Schema); // false

try {
  mySchema.parse('');
} catch (error) {
  console.log(error instanceof z.ZodError); // false
}

// -----------------------------

const localSchema = z.object({});

console.log(localSchema instanceof z.Schema); // true

try {
  localSchema.parse('');
} catch (error) {
  console.log(error instanceof z.ZodError); // true
}

I have also created a demo project.

I call the package that I create schemas in mylib and the package that I'm using those, is called myprogram. I'm using npm link to add mylib to myprogram package. And also, I'm transpiling the mylib with npx tsc. Then you can test the code in myprogram with npx tsx index.ts.

A maybe related thing that I read about this. But I also checked that you are addressing this issue on ZodError class constructor.

@joepetrillo
Copy link

joepetrillo commented Apr 4, 2023

I am also having this same issue! Any updates?

@igalklebanov
Copy link
Contributor

igalklebanov commented Apr 11, 2023

Hey 👋

myprogram simply has 2 versions of zod installed - it's own dependency, and mylib's dependency.

npm why zod output:

zod@3.21.4
node_modules/zod
  zod@"^3.21.4" from the root project

zod@3.21.4
../mylib/node_modules/zod
  zod@"^3.21.4" from mylib@undefined
  ../mylib
    mylib@undefined
    node_modules/mylib
      mylib@"file:../mylib" from the root project

Make zod a peerDependency + devDependency @ mylib (instead of a dependency), and just switch to pnpm. 🤷

@ahmafi
Copy link
Author

ahmafi commented Apr 11, 2023

Thanks, it solved the issue. However, if I set the "type": "module" in the myprogram's package.json then the issue occurs again.

I also had to remove the node_modules in myprogram and run npm install again.

Also, another question. Are these kinds of problems less common in pnpm?

@gtflip
Copy link

gtflip commented Jun 3, 2023

I ran into this problem with one of our internal libraries. It's because it checks the prototype and the instances from different copies of the library have different prototypes. It's the reason we have Array.isArray(), which works across different "realms", like different iframes.

I solved it by overriding the [Symbol.hasinstance] function. I'm still not sure it was the best way to do it, but it solved the problem and didn't require changing all the times I used instanceof in other libraries.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof

@KhaledElmorsy
Copy link

Same problem here. Tried setting it as a peer dependency in the consumed package and made sure that the workspace only has one /zod folder, and still cant solve this.

@rodeyseijkens
Copy link

So yea because of version differences between the packages.

Not sure if there is a good way to handle this besides having the same version.
Maybe it would be a solution to have the following?

import { mySchema } from 'mylib';
import { z } from 'zod';

try {
  mySchema.parse('');
} catch (error) {
  console.log(error instanceof mySchema.ZodError); // true
}

@muratgozel
Copy link

Temporarily did if (error.constructor.name === 'ZodError'/* error instanceof z.ZodError */)

@mmkal
Copy link
Contributor

mmkal commented May 31, 2024

This can also happen even with a single zod installation, without iframes or anything, if one part of the code accesses the commonjs export (main in package.json) and another part accesses the esm export (module).

Issue in trpc-cli: mmkal/trpc-cli#7

If anyone's interested, a non-bullet-proof-but-in-most-cases-good-enough fix is to look at the constructor name as above, but you should also account for sub-classes:

export const looksLikeInstanceof = <T>(value: unknown, target: new (...args: any[]) => T): value is T => {
  let current = value?.constructor
  do {
    if (current?.name === target.name) return true
    current = Object.getPrototypeOf(current) as Function
  } while (current?.name)
  return false
}

PR in trpc-cli: mmkal/trpc-cli#8

CC @colinhacks in case you know of some reason why the method above wouldn't be a good idea for some reason. Given zod ships both commonjs and esm, so has the dual-package hazard, it would be nice if it used Symbol.hasInstance as @gtflip suggests to avoid this out of the box.

Also related: evanw/esbuild#3333

@colinhacks
Copy link
Owner

colinhacks commented May 31, 2024

Oh wow. Embarrassingly I wasn't aware of Symbol.hasInstance and it is such a game changer. I've seen multiple issues relating to dual package issues and for Zod 4 I was looking into a wat to partly avoid it (with an ESM wrapper that re-exports CJS, as recommended in the Node.js docs).

But of course there are always scenarios where multiple versions of Zod will be floating around.

In Zod 4 all schemas will have a kind property (e.g. kind = Symbol.for("ZodString")) and I'll implement hasInstance with a simple symbol comparison.

@gtflip
Copy link

gtflip commented May 31, 2024

Yeah, it took me a bit of searching for a solution before I found it. I also ran into it with one of our internal packages, which is why I had to find a solution. 😃

I had difficulty finding example implementations for my use case, but I store an object at a key in each instance of my class and in Symbol.hasInstance I check to see if that key exists, the key contains an object of the right shape, and the values in the shape are what I expect. If it is, I check that the return of this.getType() is what I expect it to be.

@andresgutgon
Copy link

Related. Hard lesson learned here 😂
IdoPesok/zsa#121 (comment)

@tianhuil
Copy link

tianhuil commented Jul 12, 2024

Hi @colinhacks: we're running up against this issue. Would you accept a contribution with Symbol.hasInstance?

@MarijnMensinga
Copy link

Note to self (and hopefully others):
What fixed the problems for us was to import zod consistently like this: const {z} = require('zod'); instead of mixing imports with const z = require('zod');

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests