#Types are not inferred correctly for arguments on callback when using builder pattern

40 messages · Page 1 of 1 (latest)

last kernel
#

I am running into an issue with generics, it is hard to explain but I will try to explain in the thread, the basic issue is that on line 129 body is typed as any
https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAMQIYGcbAGYE8A0cDeiq62ASgKZgA2uRamWFAjgK7lpwC+cGUEIcAOQZiDQQChQkWATolGEFjHIBhCADsMwAOZcefAcNHYA9DCxh2JvkvKCA3JPDR4hAOLl15KMADGAFQtyPV5+IXNLFF8fMBgHJ2l4ACo4VDgAL30wwXSIABMJcXETEzglYCpgczgI9nFauAAtfIA1JEq8pBhoOABeDIA6ZrzAywAeJHVaSempgD5HeqC4AAUDYBRyAHkoADkIGDH-Ob64fzgAH1X1zaOFooaAUQAPMEm8u9Pz8mfldTyUHBgJpvHAtnAAPyyADaAGkgeo4ABrchYCAYMEAXQAXGC4Zi9LivAA3byLBr7dQrJCwYDtT79fDiOAsuDQlYI5Go9FnTEAWghuP87Mxjk4ixKTXyNWWGB6rG8WDgaB86l0xPawE66A0S0scAAyjBVdodkaTQBBKBQJBYA2+AAW5BASFO6SG+XNwN0V3dwytNqwYz9nuN3vuDWGXrVAdtWwARgArci+GD2p0u05M1lslFK4HKsNqnGGoumqDR7Sxu2O51IMWOSOhk0J5Op9N1rPM1nQvOclXekshvKVht64LDNodLrQVsptNlnbVyuA-rD0bkMZRsvVuft2suvAzI-zRvLSea7WzpPzytuj0jIJb5vevdpg9IE+zLAR5akRTKAAsuQMAOtK-SCJAaCCJcQjaCBMFXJBSiIUIeTkFQIF2LBkFdI68QNP+tgeF4Pi+F2ObxvkWDYhe07dFAODdiyCpQHaZa0a0l4zlAb6Vku1q2iuTE5m8NogCgnF5FOWo8XxZYiayTpIOhUCSXRskMfJJqKSyLBQFQ2IDmqulwCAIFgXk2JEUBFn5KZUDsJA6ibNix5FJwDx-gB5AbkcG5wD8fwAnANnkCR3h+CcjLMXAVF5DR-gbtCgjxVggiYqZrHsSa2JJUEKXZZWGWmWJSASXlyW4eJKAlbFymqZJ+WWClDXeLVmWxfphnGdopnmaB+TWT5wGDXkDlORornuTmDrvJhUDDbYAAS83eP5QRzOInlefqYWrf8C1HAdeQLQFQWeCFYURWR0VwAAFLFZUVQQ2pINi2Y5qyaW4u6wIYOt-gnWdBWpdRGVzHgsU5kVHGDP9gPA94VWwyaENQ19rLPZJ8MglAx1rVAVXY+j0NKeQKntb9AwI-jQOE1VbVqRDsVQ5wnAAJR9Ccaz8Bs2x7AcBOHcjoOOSgzmbBD5J-pQVBIL45CwqiRxhedvyXYC1K0vSatPtdniRb4cyQ2cytKhd-yAnmPL+Hrlh4M15B3Z9LLspyNsYnbPkbiWHKWyF-jm5CZwBUK9vkCK22LOIPyJHAvjyyggJharPvLAHWs0uguvp+MBukVFcBzP0EdjAXRsm67cAAMQDZZ4d55Hgj1-kGWnCw-zkFoXh5I4OY128oGN7YVXde3-Sd+hPfkH3sU1z9ZwRylaXt1cU-d8Cs-96yNeo96I-KCjbBscVBKT13M9zwPTM497o+g7fa9lJfW-X7v2OH75oMkwS6+v73HeLIa7i0luQL+VVQFTTsH-F+0835ANrnNEWi0l5N1aoTZ+G8r6LBzPBeArc8j3Q5tXHMjkYD6URKBDYAw652TyLFHaeCQJwCHg6YhpDWTkMoTUB0NDB5dAdIwooOZJQAEl1BVDpJUdIwRQLBBsMoOAAB3KoDpeHBGWklDkhDUj-FYYI2KaAuh+FYRANA902G4l6hzXEqcKDUAVkrFWZcK63TwC3ehggPFQTiCbAgZMWS+CmvARRwR+heGUaFHyxDEFkJ8rQ3REFfHxExiyMJtC2GnDYXErhIEeFhMQUw1kkoABC1E4AanotAWKaUNr6kzmgh+DszhWl0I0p2K9waYjmPdKIGZ3r+DabY6Jthy5yycebep5APGr0dm0-xnCWTUJQLQtKpx+l1lyek-JUAqF8JQEUkRJTSgAEUT5Kl6pU7iDFYr7zVNMwKGsrZNKPkEeZUB2nPMDsfRUZ9embJdHlYZdifLjMcYrKZTsPH3O0N41pnzFmBN4fw2FGyPzbLgNwvZKLDnCNipKZaFNGrXOqVAeqxL2qPI6RHD5XzgqAk6YIJ+PS+kfmBZ8kZ9iJmQpVtCoQT86VIrSbi2ht90UDMxdi-ZGwjkEtKNrcqgIqmaRqaJGkSrqXfMZbShF9LNahx-hqiSEM2UDI5doLlYKHHy15UGfl1UlXwqGYik4SzRUCJqhKrZyLpW4rlbNQmWqGWvO-i0l1+qXlMuQadbwprfDxgtVasZNrJl8o3B4mNC1nULLdcilZYrCanATVK3ZMq8U5k8jtYJLl4AqqvFAe8EAbypnutXXq2J3S9WIaZdQLAQDxm8J2gYfaB3eB7bFGkgZh1TttPdLtZZiEcyYpzIoNaODrNLgk3x91BAsE2MzDmAw0r3XrTxI9Wbx1trepzbmAScxvWPdRbaHMgA

#

I am trying to type the paramaters to the handler function as the type of the body (and the other paramaters) When I add this

bodyType():z.infer<TRouteType['body']>{
        return {} 
    }

And get the type like this

const bodyParam = Route.post('users').body(validator).bodyType()

I get the correct type, this tells me that TRouterType['body'] is correctly passed, but for some reason it is not passed to the generic in the return type of handler I am only having this issue in handlers, in any other function the state is updated correctly

silent dawn
#

@last kernel Not exactly sure what's wrong, but I would approach it in a slightly different way:

echo grottoBOT
#
nonspicyburrito#0

Preview:```ts
import {z} from "zod"

export class Route<TBody, TQueryString> {
private bodySchema?: z.ZodTypeAny
private queryStringSchema?: z.ZodTypeAny

body<T extends z.ZodTypeAny>(
schema: T
): Route
...```

last kernel
#

@silent dawn the difference is using separate generics for each field correct?

#

I was trying out a more complex type because I have dozens of fields and its easier to throw it in one type

silent dawn
#

I agree having to repeat all these generics kind of suck and your replace is more convenient, but I feel like in this case it's easier to just repeat. It's just one time work anyways, presumably once you finished writing this class you will unlikely to ever add/remove its generics again.

last kernel
#

@silent dawn that's how I started out but I think the complex type is necessary if I want this to scale, I still need to implement dozens of functions (for auth) also this is a personal project to scratch an itch so I would like to figure it out, regardless I appreciate your help and maybe I can work from this to figure it out

silent dawn
#

Makes sense.

#

I don't have time to debug your types right now, but a few things that might prove useful:

#

Another way to write replace is to do:

type Replace<T, K, V> = Omit<T, K> & Record<K, V>

Which might be less finicky than mapped type.

#

Alternatively, if you start your object with unknown and then narrow it down, you won't need to replace and can simply just intersect.

type Foo = { key: unknown }
type Bar = Foo & { key: string }
// Bar is { key: string }
last kernel
#

I did that at one point, without a utility type just using & in the methods return type

silent dawn
#

Ah

#

Oh and I guess one last difference between yours and mine is that mine stores the z.infer'd type rather than the schema type as the generic.

#

I'm not sure if any of these are what made it work, but could be worth looking into.

#

In my head I'm thinking a solution where generic represents the callback's argument data type, and it starts with a default type of { body: unknown, queryString: unknown, ... }, then each schema build step returns Route<T & { body: z.infer<TSchema> }>.

last kernel
#

Doing it this way means I lose the type safety for the validation object inside the class (unless I pass two generics to Route) I am gonna try it out and see if it makes a difference, I kind of hope it doesn't because if it does my whole understanding of generics is wrong

silent dawn
#

I'm fairly certain you will introduce unsoundness at some point, because you are mutating the class' generics.

#

In your original code, you can see your return this in builder steps are erroring and require assertion.

last kernel
#

I need the class to be a generic for the next step, I want to use the class type as a generic for a client side fetch wrapper

silent dawn
#

Yes the generic isn't the problem, I'm just saying you will have to introduce unsoundness at some point, so "I lose the type safety" isn't really a problem because you always have to cast.

#

Casting at return this as ... is not much different from casting at this.bodySchema = schema as ....

last kernel
#

This is weird, I changed this line from

handler<TType extends TRouteType, TArg extends TType['handler']>(cb:TArg): Route<ReplaceKey<TType, 'handler', TArg>> {
        this.#handler = cb;
        return this as unknown as  Route<ReplaceKey<TType, 'handler', TArg>>;
    }

To

handler<TType extends TRouteType, TArg extends (props:{data:{
        body:z.infer<TType['body']>,
        queryString:z.infer<TType['queryString']>,
        params:z.infer<TType['params']>,
        headers:z.infer<TType['headers']>
    }}) => any>(cb:TArg): Route<ReplaceKey<TType, 'handler', TArg>> {
        this.#handler = cb;
        return this as unknown as  Route<ReplaceKey<TType, 'handler', TArg>>;
    }

And it works, weird how it only infers the correct type if it is defined on the handler and not passed from the generic, I don't get why but for now it is enough to continue

silent dawn
#

I gave it a shot earlier:

echo grottoBOT
#
nonspicyburrito#0

Preview:```ts
import {z} from "zod"

class Route<
TSchema extends Record<
"body" | "queryString",
z.ZodUnknown

{
private schema = {
body: z.unknown(),
queryString: z.unknown(),
} a
...```

#
nonspicyburrito#0

Preview:```ts
import {z} from "zod"

class Route<
TSchema extends Record<
"body" | "queryString",
z.ZodUnknown

{
private schema: TSchema

constru
...```

silent dawn
#

They are both essentially the same, just differ in how the builder handles building next step. One modifies and returns itself (which is unsafe and require casting) while the other constructs a new instance (safe and no casting needed)

#

I don't really like the types for callback though, it's ugly.

#

I still prefer the version which generic represents the already z.infer'd type rather than schema's type.

#

Eh I suppose I'll refactor the z.infer version to single generic for completeness.

echo grottoBOT
#
nonspicyburrito#0

Preview:```ts
import {z} from "zod"

type Schema<T> = {
[K in keyof T]: {
parse: (data: unknown) => T[K]
}
}

export class Route<
TData extends Record<"body" | "queryString", unknown>

{
private
...```

silent dawn
#

You'd need to change the Schema<T> to map to a zod type that can parse into T[K], I'm not familiar enough with zod's internal to know how to make that happen so I just used a duck type as example, but yeah Schema<T> makes the implementation safe, as you can see down in the handler method (you wouldn't be able to use parsedBody in place of parsedQueryString)

last kernel
#

I don't have the headspace to look at this now but at a quick glance it seems like ti solved my issue, now I just have to overcomplicate it

#

@silent dawn Thanks a million

silent dawn
#

Looks nice.

last kernel
silent dawn
#

The links seem to be dead already