Open ChuckJonas opened 1 year ago
Hi,
No problem, I consider it part of the documentation.
People often mention higher kinded types when this use case comes up, but no implementation of HKT in TS can apply a generic function type with an argument dynamically, because the parameters in a generic function are not of the same kind as the parameters in a generic type alias or interface and TS doesn't provide any hook to access them at the type level.
However type-lenses may help if you are ready to take a risk.
First, let me suggest something simpler that we can do at the value level, because it may be enough for your use case.
declare function validate <Request>(req: Request): { validated: Request };
// we can actually do this at the value level
const validateFooString = validate<Paths['/route']['get']['request']>
const api: Api<Paths, typeof validateFooString> = {
before: validate,
api: {
"/route": {
get: (req, before) => {
before.validated
// ^? (property) validated: { foo: string; }
return (before as any).validated.foo
}
}
}
}
Now if we want to do this dynamically, an idea is to replace unknown
in ReturnType<B>
with P[path][method]['request']
, which is not what we want semantically but yields the same result in this instance.
type-lenses require a path to replace some piece of type with another. We can write a utility type that looks for unknown
in an arbitrarily nested object and generates the path to go there.
// create a path leading to `unknown` in the object type
type CreatePath<T> = FilterPath<GetPaths<T>>
// get all possible paths and the value they lead to in the object type
type GetPaths<T> = T extends object
? { [K in keyof T]: [K, ...GetPaths<T[K]>] }[keyof T]
: [T];
// exclude paths which don't lead to `unknown`
type FilterPath<T> = T extends [unknown, ...unknown[]]
? IsUnknown<Last<T>> extends true ? Init<T> : never
: never;
With this path it is now possible to leverage Replace
// in API
before: Replace<CreatePath<ReturnType<B>>, ReturnType<B>, P[path][method]['request']>
No change needs to be made to api
const api: Api<Paths, typeof validate> = {
before: validate,
api: {
"/route": {
get: (req, before) => {
before.validated
// ^? (property) validated: { foo: string; }
return (before as any).validated.foo
}
}
}
}
Now the risk I mentioned is that unknown
is a perfectly valid type which could appear multiple times in an object, and not necessarily in places you want to find/replace, so use this with caution!
First off, seriously thank you so much for taking the time to write out such a lengthly and complete response! 🙇
let me suggest something simpler that we can do at the value level, because it may be enough for your use case.
Unfortunately this isn't sufficient for my use case. In my attempt to make the example code as simple as possible, I removed some critical context. The goal is to get the before response type to work across various routes, based on the request types of each route:
It's going to take me a bit to fully grok this, but it definitely seems inline with what I'm looking for!
I do have one immediate followup question....
Why, if I replace the return type of validated with something that doesn't use the generic parameter Response
, does the type before
becomes the same as request
?
I assume it has something to do with the conditional types used here Replace<CreatePath<ReturnType<B>>, ReturnType<B>, P[path][method]['request']>
somehow looking for the type of P[path][method]['request']
and falling back to P[path][method]['request']
when it's not found?
You're welcome.
The issue here is that CreatePath<{ validated: boolean }>
returns never
, because there is no path leading to unknown
. The behaviour of Replace
is unspecified when no path is provided.
I would compare CreatePath<ReturnType<B>>
with never
and branch over that
before: CreatePath<ReturnType<B>> extends infer Path extends Query
? [Path] extends [never]
? ReturnType<B>
: Replace<Path, ReturnType<B>, P[path][method]['request']>
: never
Note that you need to import the constraint Query
from type-lenses.
This reminds me that when using ReturnType
on a generic function, if the generic has a type constraint, it's going to be substituted with the type constraint instead of unknown
, and as a result CreatePath
won't detect any path either. One way to deal with this could be to look for supertypes of P[path][method]['request']>
instead of unknown
when generating the path, but as you can imagine the risk of collision with legitimate types becomes very high.
Awesome thanks!
I've been messing around with type-lenses
for the last hour and starting to get it. Still TBD if I'll have the skills to incorporate this back into the full vision, but such a cool little utility library!
I think I enjoy writing types more than the code itself 😅
Yeah that's something to be wary of actually ;)
By the way, this issue made me consider including path finding to type-lenses, and changing the behaviour of Replace
when the path is never
so that users don't need to branch.
Don't hesitate to post issues if the documentation wasn't very clear. I don't get a lot of feedback.
type-lenses now implements FindReplace
for this kind of use case, so this:
before: CreatePath<ReturnType<B>> extends infer Path extends Query
? [Path] extends [never]
? ReturnType<B>
: Replace<Path, ReturnType<B>, P[path][method]['request']>
: never
can be re-written as:
before: FindReplace<ReturnType<B>, unknown, [P[path][method]['request']]>
Notice the brackets around
P[path][method]['request']
.FindReplace
expects a tuple of replace values (or a replace callback)
Replace
now fails gracefully when the query is never
, and a general FindPath
utility type was added to the library, so the following would also work:
before: Replace<FindPath<ReturnType<B>, unknown>, ReturnType<B>, P[path][method]['request'], any>
Notice the
any
at the end.Replace
now type checks the replace value against the query, which is problematic when it requires a generic to be resolved (hereB
).any
lets us disable the check.
I ended up here from this typescript issue. and think this library might solve the problem I'm running into.
I'm trying to create an library interface where a generic function can be passed in and the type basically resolved from a different context:
Hopefully this Typescript-Playground isn't too hard to follow.
Sorry for such a direct question, please close/delete if this isn't appropriate.