The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
#Types are not inferred correctly for arguments on callback when using builder pattern
40 messages · Page 1 of 1 (latest)
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
@last kernel Not exactly sure what's wrong, but I would approach it in a slightly different way:
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
...```
You can choose specific lines to embed by selecting them before copying the link.
@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
I'm not sure if that's really the difference maker, but rather your type was too complex so I kind of just gave my own shot and that seems to work.
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.
@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
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 }
I did that at one point, without a utility type just using & in the methods return type
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> }>.
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
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.
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
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 ....
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
I gave it a shot earlier:
Preview:```ts
import {z} from "zod"
class Route<
TSchema extends Record<
"body" | "queryString",
z.ZodUnknown
{
private schema = {
body: z.unknown(),
queryString: z.unknown(),
} a
...```
You can choose specific lines to embed by selecting them before copying the link.
Preview:```ts
import {z} from "zod"
class Route<
TSchema extends Record<
"body" | "queryString",
z.ZodUnknown
{
private schema: TSchema
constru
...```
You can choose specific lines to embed by selecting them before copying the link.
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.
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
...```
You can choose specific lines to embed by selecting them before copying the link.
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)
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
If you're curious what I am building I started writing the blog post, should be an interesting project
Welcome to my website!
Looks nice.
Weirdly this one works
But this one doesn't
The relevant parts are exactly the same
(I need short links because the playground links are too long)
The links seem to be dead already