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

Can't generically compose + transform schema without type error #2146

Closed
johtso opened this issue Mar 5, 2023 · 16 comments
Closed

Can't generically compose + transform schema without type error #2146

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

Comments

@johtso
Copy link
Contributor

johtso commented Mar 5, 2023

Say I want to do something like this:

https://tsplay.dev/mq9bjm

import { z } from 'zod'

async function stripOuter<TData extends z.ZodTypeAny>(schema: TData, url: string): Promise<TData> {
  const zStrippedResponse = z
    .object({
      topLevelKey: schema,
    })
    .transform(data => {
      return data.topLevelKey
      //     ^?
    })
  
  return fetch(url)
    .then(response => response.json())
    .then(data => zStrippedResponse.parse(data))
}

I get the following type error:

Property 'topLevelKey' does not exist on type '{ [k in keyof baseObjectOutputType<{ topLevelKey: TData; }>]: baseObjectOutputType<{ topLevelKey: TData; }>[k]; }'.

Is this something I should be able to do? Any pointers where I'm going wrong?

@JacobWeisenburger
Copy link
Collaborator

JacobWeisenburger commented Mar 5, 2023

Bug confirmed

function foo<Schema extends z.AnyZodObject> ( schema: Schema ) {
    return z.object( { bar: schema } ).transform( x => x.bar )
    //                                                   ^^^
    // Property 'bar' does not exist on type
    // '{ [k in keyof baseObjectOutputType<{ bar: Schema; }>]: baseObjectOutputType<{ bar: Schema; }>[k]; }'.
}

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

This should be improved in zod@canary, give it a shot @johtso

@johtso
Copy link
Contributor Author

johtso commented Mar 6, 2023

@colinhacks all working with the latest release! You're an absolute legend getting that out so fast!

Is there by any chance a way to avoid having to assert that the generic schema is non-null?

return data.nested!;

I feel like I actually want to type TData as something less generic.. I want to type it as any zod object schema.
Is there a way to do that? I tried TData extends z.ZodObject<z.ZodRawShape> but it's still saying data.nested could be undefined. I know the provided schema cannot result in undefined so it would be nice to be able to type it as that rather than using a non-null-expression.

Edit:
Just tried z.deoptional<z.SomeZodObject> and no luck with that either.

@JacobWeisenburger
Copy link
Collaborator

JacobWeisenburger commented Mar 6, 2023

@colinhacks

Still a problem in zod@canary

import { z } from 'npm:zod@canary'

function foo<Schema extends z.AnyZodObject> ( schema: Schema ) {
    return z.object( { bar: schema } ).transform( x => x.bar )
    //                                                   ^^^
    // Property 'bar' does not exist on type
    // '{ [k in keyof baseObjectOutputType<{ bar: Schema; }>]: baseObjectOutputType<{ bar: Schema; }>[k]; }'.
}

@johtso
Copy link
Contributor Author

johtso commented Mar 6, 2023

@JacobWeisenburger canary releases don't seem to be triggering, I think this should fix it? #2148

@daguitosama
Copy link

Im experiencing a very similar issue with the safeParse function return type:

function parseNameAndUpdateNameError(name: string): string {
    const nameSchema = z.string().min(1).max(10).trim()
    const result = nameSchema.safeParse(name);
    if(!result.success){
        var nameErrorMessage = result.error.issues[0].message;
        // update logic
    }
    return result.data;
    //            ^^^^
}

/**
* Error:
*   Property 'data' does not exist on type 'SafeParseReturnType<string, string>'.
*   Property 'data' does not exist on type 'SafeParseError<string>'.ts(2339)
*/

I have tested on 3.21.2 and the canary versions.

@JacobWeisenburger
Copy link
Collaborator

JacobWeisenburger commented Mar 6, 2023

@daguitosama this is an unrelated issue.

you need to add else

function parseNameAndUpdateNameError ( name: string ): string {
    const nameSchema = z.string().min( 1 ).max( 10 ).trim()
    const result = nameSchema.safeParse( name )
    if ( !result.success ) {
        var nameErrorMessage = result.error.issues[ 0 ].message
    } else {
        return result.data // no errors, yay!
    }
}

or return early

function parseNameAndUpdateNameError ( name: string ): string {
    const nameSchema = z.string().min( 1 ).max( 10 ).trim()
    const result = nameSchema.safeParse( name )
    if ( !result.success ) {
        var nameErrorMessage = result.error.issues[ 0 ].message
        return nameErrorMessage
    }
    return result.data // no errors, yay!
}

@daguitosama
Copy link

O I see, thanks so much for the help, I really appreciate it !

@JacobWeisenburger
Copy link
Collaborator

If you found my answer satisfactory, please consider supporting me. Even a small amount is greatly appreciated. Thanks friend! 🙏
https://github.com/sponsors/JacobWeisenburger

@daguitosama
Copy link

I wish I could man, I barely own a computer, not the best financial time for me 🥲

But, I have some ideas that might help to get more support:

  • spin up a some youtubes featuring how to craft things with the parse, don't validate mantra using zod (I will definitely subscribe and share 🤣)
  • little egghead-like course to go even further and build a simple but real prod-ready prototype featuring the mantra on the server side in every boundary of the app ui-form->server-code<-db-call. I have't seen a really good material of that out there.

If you need more ideas, will more then happy to share more on the twitters/discords...

@colinhacks
Copy link
Owner

Thanks @johtso that fixed the CI 👍

@JacobWeisenburger that should be working now

@johtso
Copy link
Contributor Author

johtso commented Mar 6, 2023

@colinhacks just in case it got lost amongst the other messages, any thoughts on this? #2146 (comment)

I can totally just live with telling typescript that the value will never be undefined, but very curious as to whether Zod already offers something for me to type my generic more accurately, or if it's something that might be worth adding..

@colinhacks
Copy link
Owner

colinhacks commented Mar 6, 2023

Yeah, I noticed that and tried to fix it for a while. Then I realized it makes sense. TData can be any ZodSchema, including ZodOptional. When Zod "computes" the inferred type of a ZodObject, it adds the ? to any schema might be optional.

Screenshot 2023-03-06 at 1 38 54 PM

Because TData (which has inferred type any) might be optional (any extends undefined is true), the question mark gets added. It's weird but I think it's consistent & encourages defensive programming.

@johtso
Copy link
Contributor Author

johtso commented Mar 6, 2023

@colinhacks would it not make sense to be able to have my generic type variable extend something more specific in this case? I know I'm going to be dealing with non-optional z.object schemas. And more generally does it make sense to be able to type generics in narrower ways?

Feels like I should be able to type the argument as a non-optional object schema. z.deoptional<z.SomeZodObject>

And if I tried to give the function z.object({}).optional() as an argument I'd get a type error.

@johtso
Copy link
Contributor Author

johtso commented Mar 6, 2023

Say I did something like this:

const nonOptionalObject = z.object({}).required()
function foo<TSchema extends typeof nonOptionalObject>(schema: TSchema, data: any) {
    const zUnwrapped = z
      .object({
        nested: schema,
      })
      .transform(data => {
        return data.nested
      })
   return zUnwrapped.parse(data)

Shouldn't the types convey the fact that the schema cannot result in undefined?
I'm basically losing type information.

@colinhacks
Copy link
Owner

@johtso Hm that is curious.

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

4 participants