Link to playground:
I've been sitting with this for a while now and I've been trying to draw some inspiration from previous answers with no luck
I basically have the an object like this:
const query = {
fn: () => Promise<any>,
fn2: (arg: T) => T
fn3: (arg: T) => T 2
}
and then I'm running it in a function where i extract all of the methods and run them sequentially:
// import * as queries
const seqFn = <T keyof typeof queries>(query: T) => {
const { fn, fn2, fn3 } = queries[api]
const res = await fn()
const res2 = fn2(res)
return fn3(res2)
}
which works nicely as ts is capable of inferring the types going in and out of every function, which means i also get type safety for the return function of seqFn.
However, now I would like to extract the import and make it dependency injected instead:
const seqFn = (queries) => <T keyof typeof queries>(query: T) => {
const { fn, fn2, fn3 } = queries[api]
const res = await fn()
const res2 = fn2(res)
return fn3(res2)
}
and this is where i run into problems. I would like to keep the same behaviour as above with the inference but no matter how i type it I only manage to get the keys inferred, not the functions themselves. I've tried different variants but I always end up in something like this:
type queryTypes<K extends querymain> = {
[key in keyof K]: {
fn: K[key]['fn']
decoder: K[key]['fn2']
transformationFn: K[key]['fn3']
}
}
type inferQueryTypes<T extends querymain> = T extends queryTypes<T>
? queryTypes<T>
: never
export const queryFnn =
<K extends querymain>(queries: inferQueryTypes<K>) =>
async <T extends keyof inferQueryTypes<K>>(api: T) => {
const { fn, fn2, fn3 } = queries[api]
const response = await fn()
const decoded = fn2(response)
return fn3(decoded)
}
I'm obviously doing something wrong but what exactly? Could somebody point me in the right direction?
CodePudding user response:
The function needs a generic constraint so that the general shape of K
is known. This allows us to do K[T]["fn3"]
for the return type of the function.
type ValidateQueries<T extends Record<string, {
fn1: () => Promise<any>
fn2: (...args: any[]) => any
fn3: (...args: any[]) => any
}>> = {
[K in keyof T]: T[K] & {
fn1: () => Promise<any>
fn2: (arg: Awaited<ReturnType<T[K]["fn1"]>>) => any
fn3: (args: ReturnType<T[K]["fn2"]>) => any
}
}
const queryFn = <
K extends Record<string, {
fn1: () => Promise<any>
fn2: (...args: any[]) => any
fn3: (...args: any[]) => any
}>
>(q: ValidateQueries<K>) => async <T extends keyof K>(api: T): Promise<ReturnType<K[T]["fn3"]>> => {
const { fn1, fn2, fn3 } = q[api]
const response = await fn1()
const decoded = fn2(response)
return fn3(decoded)
}
We need the explicit return type because the compiler eagerly determines a "wrong" or too general type for fn3
. Auto-inference would therefore lead to a return type of Promise<any>
.
const res = queryFn(q)('queryObj')
queryFn({
queryObj: {
fn1: () => Promise.resolve(1),
fn2: (arg: string) => arg 3, // Error
fn3: (arg: number) => arg 1,
}
})('queryObj')