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

Inconsistent typing of parse result when using generic schema types #2153

Open
johtso opened this issue Mar 7, 2023 · 10 comments
Open

Inconsistent typing of parse result when using generic schema types #2153

johtso opened this issue Mar 7, 2023 · 10 comments
Labels
bug-confirmed Bug report that is confirmed

Comments

@johtso
Copy link
Contributor

johtso commented Mar 7, 2023

Following on from #2146
I'm still having issues getting return values of functions that accept generic schemas to work right.

It now works perfectly if I'm nesting the schema inside a new schema. But if I'm simply using the schema as-is, the result type doesn't get inferred and gets typed as any.

https://tsplay.dev/mZ1zJN

import { z } from 'zod'

const zSchema = z.object({
  name: z.string()
})

function validate<TSchema extends z.ZodTypeAny>(
  thing: any,
  schema: TSchema,
) {
  return schema.parse(thing)
}

// typed as any
const result = validate({ name: "zoddy" }, zSchema)

function validate2<TSchema extends z.ZodTypeAny>(
  thing: any,
  schema: TSchema,
) {
  const nested = z.object({
    nested: schema
  })
  return nested.parse(thing)
}

// typed as { nested: { name: string; } }
const result2 = validate2({ nested: { name: "zoddy" }}, zSchema)
@johtso
Copy link
Contributor Author

johtso commented Mar 7, 2023

I can get the correct types if I do:

return schema.parse(thing) as z.infer<TSchema>

But shouldn't we be able to have the return type inferred without having to do that?

@johtso
Copy link
Contributor Author

johtso commented Mar 7, 2023

As a better example let's say we're returning the result of safeParse, for which you can't just do z.infer<TSchema>.

Taking a look at Matt's zod-fetch I saw a bit of type magic he was doing: https://github.com/mattpocock/zod-fetch/blob/28b3c5a33769243a350d921eb2dae7bd08e8bf1b/src/createZodFetcher.ts#L10-L12

The thing is, that then works beautifully if you're just calling safeParse on the schema and infers the return type beautifully, but then you get type errors if you try to manipulate it in any way (because it's not a proper Zod type).

Is it not possible to have the best of both worlds? A generic Zod type that allows the return type to be inferred?

https://tsplay.dev/Nr8VoW

import { z } from 'zod'

const zSchema = z.object({
  name: z.string()
})

export type Schema<TSafeParseData> = {
  safeParse: (data: unknown) => TSafeParseData;
};

function validate<TSafeParseData>(
  thing: any,
  schema: Schema<TSafeParseData>,
) {
  return schema.safeParse(thing)
}

// typed as:
// z.SafeParseReturnType<{
//     name: string;
// }, {
//     name: string;
// }>
const result = validate({ name: "zoddy" }, zSchema)

function validate2<TSafeParseData>(
  thing: any,
  schema: Schema<TSafeParseData>,
) {
  const nested = z.object({
    // Type 'Schema<TSafeParseData>' is missing the following properties from type 'ZodType<any, any, any>': _type, _output, _input, _def,
    nested: schema
  })
  return nested.parse(thing)
}

const result2 = validate2({ nested: { name: "zoddy" }}, zSchema)

@JacobWeisenburger
Copy link
Collaborator

seems like a bug, not sure how to fix it

@JacobWeisenburger JacobWeisenburger added the bug-confirmed Bug report that is confirmed label Mar 7, 2023
@colinhacks
Copy link
Owner

colinhacks commented Mar 7, 2023

You need to do this:

function validate<TSchema extends z.ZodTypeAny>(thing: any, schema: TSchema) {
  return schema.parse(thing) as z.infer<TSchema>;
}

The result of schema.parse(thing) will resolve to any inside the body of the function. TypeScript isn't smart enough to do what you want automatically. You need to remind it with type casts.

Basically TypeScript will stop propagating the generic at a certain point and try to resolve everything to "real" types.

Put another way, don't rely on inferred return types with generic functions.

@johtso
Copy link
Contributor Author

johtso commented Mar 7, 2023

@colinhacks is there an equivalent for the return value of safeParse?

@johtso
Copy link
Contributor Author

johtso commented Mar 7, 2023

This approach seemed promising.. the return value gets inferred automatically when returning schema.parse(), and composing it into another schema doesn't throw any type errors. Unfortunately the return value after composing the schema is typed with an any.

https://tsplay.dev/N9jg7m

import { z } from 'zod'

const zSchema = z.object({
  name: z.string()
})

type SafeParseReturn = z.SafeParseReturnType<unknown, unknown>

interface Schema<TData, TSafeParseData extends SafeParseReturn> extends z.ZodTypeAny {
  parse: (data: unknown) => TData;
  safeParse: (data: unknown) => TSafeParseData;
};

function validate<TSafeParseData extends SafeParseReturn>(
  thing: unknown,
  schema: Schema<any, TSafeParseData>,
) {
  return schema.safeParse(thing)
}

// inferred as:
// z.SafeParseReturnType<{
//     name: string;
// }, {
//     name: string;
// }>
const result = validate({ name: "zoddy" }, zSchema)

function validate2<TSafeParseData extends SafeParseReturn>(
  thing: any,
  schema: Schema<any, TSafeParseData>,
) {
  const nested = z.object({
    nested: schema
  })
  return nested.safeParse(thing)
}

// inferred as:
// z.SafeParseReturnType<{
//     nested?: any;
// }, {
//     nested?: any;
// }>
const result2 = validate2({ nested: { name: "zoddy" }}, zSchema)

@colinhacks
Copy link
Owner

function validate<TSchema extends z.ZodTypeAny>(thing: any, schema: TSchema) {
  return schema.parse(thing) as ReturnType<TSchema["safeParse"]>;
}

@stale
Copy link

stale bot commented Jun 8, 2023

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 stale bot added stale No activity in last 60 days and removed stale No activity in last 60 days labels Jun 8, 2023
@lautarodragan
Copy link

Still relevant!

@kjetilhartveit
Copy link

Against recommended solutions I was able to get full type inference using the following pattern:

function validate<Output, Input>(thing: any, schema: ZodType<Output, ZodTypeDef, Input>) {
    return schema.parse(thing);
}

// usage
const res = validate(thing, schema);
//^ should be inferred correctly as the output of your schema

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug-confirmed Bug report that is confirmed
Projects
None yet
Development

No branches or pull requests

5 participants