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

z.ZodType and z.ZodSchema do not seem to work as I expect #652

Closed
amonsosanz opened this issue Sep 16, 2021 · 5 comments
Closed

z.ZodType and z.ZodSchema do not seem to work as I expect #652

amonsosanz opened this issue Sep 16, 2021 · 5 comments
Labels
wontfix This will not be worked on

Comments

@amonsosanz
Copy link

amonsosanz commented Sep 16, 2021

Hi there!

I've discovered the lib recently and have a question that is probably silly.

I have an application that defines its domain with Typescript, which I want to be the single source of truth. This means I'm not interested in inferring static types from Zod, but the other way around. I want Zod to obey what Typescript dictates and want the TS compiler to let me know when any inconsistency arises. I assume the dual definition of types and parsers.

I've tried to achieve this with z.ZodType<MyType> and z.ZodSchema<MyType> with no success and I have not found a clue to follow from the readme of the project, which seems to be the only documentation available.

I've prepared a small dumb example that I hope will help you understand what I intend to do (CodeSandbox here):

import * as z from "zod";

type A = {
  foo: string;
  bar: string;
};

// This parser exactly matches interface A so we have no errors. So far so cool.
const AParser: z.ZodType<A> = z.object({
  foo: z.string(),
  bar: z.string()
});

///////////////////////////////////////////////////////////////

type B = {
  bar: string;
};

// Why I get no errors on a parser that does not match the type z.ZodType<B>?
const BParser: z.ZodType<B> = z.object({
  foo: z.string(),
  bar: z.string()
});

I have some experience with io-ts. Please find here a CodeSandbox demonstrating the result I'd expect.

Thx!!

@amonsosanz
Copy link
Author

I'm facing another issue which I believe is related.

Notice how fiatBalance is optional in the Account interface. I can though forget to add the optional() method on the parser and not get warned by the compiler:

image

@scotttrinh
Copy link
Collaborator

@amonsosanz

I believe this has something to do with structural typing in TypeScript. In the Account example fiatBalance is structurally compatible with the interface defined. I'm not aware of any way to "turn it off", but I wonder if other libraries have solved similar UX problems?

FWIW, trying to use Zod with existing types is always going to be a bit harder since it's specifically designed to be the source of truth for the types. Affordances to "check" schemas against existing types are prone to issues like the one you're encountering. If it's at all possible for you to turn this around and derive the types from your schemas, you'll have a bit better luck.

@amonsosanz
Copy link
Author

Thank you for your answer @scotttrinh!

I see... Sadly, this is a relevant limitation IMO. I think the domain of the application shouldn't be defined with a library.

but I wonder if other libraries have solved similar UX problems?

I've updated the CodeSandbox adding a third case that demonstrates how io-ts handles this. It's definitely more verbose and makes the user suffer the monads learning curve, but it's stronger type-safe-wise.

Here the snippet:

type C = {
  foo: string;
  bar?: string;
};

// This parser does not match the interface since bar is not defined as optional, and we get an error. Cool.
const CParser: t.Type<C> = t.type({
  foo: t.string,
  bar: t.string
});

// This parser does match the interface and we have no error. Cool.
const CParser2: t.Type<C> = t.intersection([
  t.type({
    foo: t.string
  }),
  t.partial({
    bar: t.string
  })
]);

For my part, you can close this issue.

Thanks again!

@amonsosanz
Copy link
Author

amonsosanz commented Dec 17, 2021

Just for reference and in case it's useful for someone else facing these problems, we've ended up applying this solution, which works quite well for us since the compiler warns us whenever there's an inconsistency between the TS types and the Zod parsers.

type Exact<T, U> = [T, U] extends [U, T] ? true : false;

const StrictSchema: <T>() => <U>(
  u: Exact<ZodSchema<T>, ZodSchema<U>> extends true
    ? Exact<Required<T>, Required<U>> extends true
      ? ZodSchema<U>
      : never
    : never
) => ZodSchema<T> =
  () =>
  <T>(u: unknown) =>
    u as ZodSchema<T>;
  • The helper type Exact returns true whenever the 2 generic types match and false otherwise.
  • The helper function StrictSchema accepts a generic type and returns a function that expects a ZodSchema as param.

I've updated the CodeSandbox to demonstrate how it works (from line 38) and it seems to work in all the cases:

  • Omit in a ZodSchema an optional prop of the TS type.
  • Forget to make optional in a ZodSchema and optional prop of the TS type.
  • Omit in a ZodSchema a required prop of the TS type.
  • Make optional in a ZodSchema a required prop of the TS type.
  • Add to a ZodSchema a prop that does not exist in the TS type.

Credits should go to @yuhr, who put us on the right track.

@stale
Copy link

stale bot commented Mar 2, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wontfix This will not be worked on
Projects
None yet
Development

No branches or pull requests

2 participants