Skip to content

Instantly share code, notes, and snippets.

@hi-ogawa
Last active May 20, 2023 07:36
Show Gist options
  • Save hi-ogawa/6d9c7252280aa8732c170abdad3fbcb4 to your computer and use it in GitHub Desktop.
Save hi-ogawa/6d9c7252280aa8732c170abdad3fbcb4 to your computer and use it in GitHub Desktop.
createReactQueryOptionsProxy.ts
//
// 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);
}
}
);
}
//
// 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