Last active
May 20, 2023 07:36
-
-
Save hi-ogawa/6d9c7252280aa8732c170abdad3fbcb4 to your computer and use it in GitHub Desktop.
createReactQueryOptionsProxy.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// generate type-safe react-query options wrapper from a record of async functions | |
// | |
type FnRecord = Record<string, (input: unknown) => unknown>; | |
type FnInput<F extends (input: unknown) => unknown> = Parameters<F>[0]; | |
type FnOutput<F extends (input: unknown) => unknown> = Awaited<ReturnType<F>>; | |
type ReactQueryOptionsProxy<T extends FnRecord> = { | |
[K in keyof T]: { | |
queryKey: K; | |
queryOptions: (input: FnInput<T[K]>) => { | |
queryKey: unknown[]; | |
queryFn: () => Promise<FnOutput<T[K]>>; | |
}; | |
mutationKey: K; | |
mutationOptions: () => { | |
mutationKey: unknown[]; | |
mutationFn: (input: FnInput<T[K]>) => Promise<FnOutput<T[K]>>; | |
}; | |
}; | |
}; | |
export function createReactQueryOptionsProxy<T extends FnRecord>( | |
fnRecord: T | |
): ReactQueryOptionsProxy<T> { | |
return createGetProxy(k => | |
createGetProxy(prop => { | |
if (prop === "queryKey" || prop === "mutationKey") { | |
return k; | |
} | |
if (prop === "queryOptions") { | |
return (input: unknown) => ({ | |
queryKey: [k, input], | |
queryFn: async () => (fnRecord as any)[k](input) | |
}); | |
} | |
if (prop === "mutationOptions") { | |
return () => ({ | |
mutationKey: [k], | |
mutationFn: async (input: unknown) => (fnRecord as any)[k](input) | |
}); | |
} | |
throw new Error(`invalid proxy property: k = ${String(k)}, prop = ${String(prop)}`); | |
}) | |
) as any; | |
} | |
function createGetProxy(propHandler: (prop: string | symbol) => unknown): unknown { | |
return new Proxy( | |
{}, | |
{ | |
get(_target, prop, _receiver) { | |
return propHandler(prop); | |
} | |
} | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// recursive variant | |
// | |
type ReactQueryProxyRecursive<T> = { | |
[K in keyof T]: T[K] extends BaseFn | |
? { | |
queryKey: unknown[]; | |
queryOptions: (input: FnInput<T[K]>) => { | |
queryKey: unknown[]; | |
queryFn: () => Promise<FnOutput<T[K]>>; | |
}; | |
mutationKey: unknown[]; | |
mutationOptions: () => { | |
mutationKey: unknown[]; | |
mutationFn: (input: FnInput<T[K]>) => Promise<FnOutput<T[K]>>; | |
}; | |
} | |
: ReactQueryProxyRecursive<T[K]>; | |
}; | |
export function createReactQueryProxyRecursive<T extends FnRecordRecursive>( | |
record: T | |
): ReactQueryProxyRecursive<T> { | |
return createGetProxyRecursive((propPath) => { | |
const keys = propPath.slice(0, -1); | |
const prop = propPath.slice(-1)[0]; | |
const run = (input: unknown) => (getByPropPath(record, keys) as any)(input); | |
switch (prop) { | |
case "queryKey": | |
case "mutationKey": { | |
return { done: true, value: keys }; | |
} | |
case "queryOptions": { | |
return { | |
done: true, | |
value: (input: unknown) => ({ | |
queryKey: keys, | |
queryFn: () => run(input), | |
}), | |
}; | |
} | |
case "mutationOptions": { | |
return { | |
done: true, | |
value: { | |
mutationKey: keys, | |
mutationFn: run, | |
}, | |
}; | |
} | |
} | |
return { done: false }; | |
}) as any; | |
} | |
// | |
// proxy utils | |
// | |
type PropPathHandler = ( | |
propPath: string[] | |
) => { done: true; value: unknown } | { done: false }; | |
function createGetProxyRecursive(handler: PropPathHandler) { | |
return createGetProxyRecursiveInner(handler, []); | |
} | |
function createGetProxyRecursiveInner( | |
handler: PropPathHandler, | |
path: string[] | |
): unknown { | |
return new Proxy( | |
{}, | |
{ | |
get(_target, prop: string, _receiver) { | |
const next = [...path, prop]; | |
const result = handler(next); | |
if (result.done) { | |
return result.value; | |
} | |
return createGetProxyRecursiveInner(handler, next); | |
}, | |
} | |
); | |
} | |
function getByPropPath(value: any, propPath: string[]): unknown { | |
for (const prop of propPath) { | |
value = value[prop]; | |
} | |
return value; | |
} | |
// | |
// typing utils | |
// | |
// bounded recursive type | |
// prettier-ignore | |
type BoundedRecursiveRecord<K extends keyof any, V> = | |
Record<K, | |
V | Record<K, | |
V | Record<K, | |
V | Record<K, V>>>>; | |
type FnRecordRecursive = BoundedRecursiveRecord<string, BaseFn>; | |
type BaseFn = (() => Promise<unknown>) | ((input: any) => Promise<unknown>); | |
type FnInput<F extends BaseFn> = Parameters<F> extends [] | |
? void | |
: Parameters<F>[0]; | |
type FnOutput<F extends BaseFn> = Awaited<ReturnType<F>>; | |
// | |
// example | |
// | |
example; | |
function example() { | |
const api = { | |
healthz: async () => ({ ok: true }), | |
videos: { | |
get: async (_: { id: number }) => ({ name: "" }), | |
create: async (_: { id: number }) => ({}), | |
destroy: async (_: { id: number }) => ({}), | |
comments: { | |
show: async (_: { videoId: number }) => [{ id: 0 }], | |
}, | |
}, | |
bookmarks: { | |
search: async () => [{ id: 0 }], | |
}, | |
} satisfies FnRecordRecursive; | |
const apiProxy = createReactQueryProxyRecursive(api); | |
apiProxy.healthz.queryOptions(); | |
apiProxy.videos.create.mutationOptions().mutationFn({ id: 0 }); | |
apiProxy.videos.comments.show.queryOptions({ videoId: 0 }); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment