I'd like to make a generic hook that takes useQuery
as a parameter and handles pagination.
That means my trpc query will always provide a skip
and a take
parameter. And it will always return a count
in the response and an array of items
.
function usePagination(query){
const [skip, take] = useSomethingThatManagesThis();
const response = query.useQuery({
skip,
take,
});
return useMemo(() => ({
items: response.data?.items,
count: response.data?.count
}), [response]);
}
function MyComponent() {
const paginated = usePagination(api.user.list);
const firstUserEmail = paginated.items?.[0].email;
}
It works in vanilla javascript, but I'm struggling to type it correctly.
I tried to naively reuse trpc client types, but I'm not able to distinguish request type (take, query) and response type (items).
import {
AnyProcedure,
AnyQueryProcedure,
inferProcedureOutput,
} from "@trpc/server";
interface BaseInput {
skip: number;
take: number;
}
type UseDatatableArgs<TData extends AnyQueryProcedure> = {
query?: {
useQuery: (input: BaseInput) => ProcedureUseQuery<
{ count: number } & AnyProcedure,
string
> & {
data: {
count: number;
items: inferProcedureOutput<TData>;
};
};
};
};
function usePagination<TData extends AnyQueryProcedure>(query){
const [skip, take] = useSomethingThatManagesThis();
const response = query.useQuery({
skip,
take,
});
return useMemo(() => ({
items: response.data?.items,
count: response.data?.count
}), [response]);
}
But I'm having no items typing in the response
function MyComponent() {
const paginated = usePagination(api.user.list);
const firstUserEmail = paginated.items?.[0].email; // paginated.items is any
}
I'm currently having a workaround by using types inferring utilities provided by TRPC, but I'd love to deduct that from the query
export type RouterOutputs = inferRouterOutputs<AppRouter>;
function MyComponent() {
const paginated = usePagination<RouterOutputs["user"]["list"]>(api.user.list);
const firstUserEmail = paginated.items?.[0].email;
}
You can make use of TypeScript's generic type inference and conditional types.
import ProcedureType
from @trpc server as well
Here ProcedureType
type represents the type of your TRPC procedure (query, mutation, etc.).
The UseDatatableArgs type will take a generic parameter TData
representing the type of the procedure, and it expects a query property that has a useQuery
function accepting BaseInput
as input and returning a ProcedureUseQuery type with the response type { count: number } & TData
.
Here's an edited copy of your code
import { ProcedureType } from "@trpc/server";
import { ProcedureUseQuery } from "trpc/react";
interface BaseInput {
skip: number;
take: number;
}
type UseDatatableArgs<TData extends ProcedureType> = {
query?: {
useQuery: (input: BaseInput) => ProcedureUseQuery<
{ count: number } & TData,
string
>;
};
};
type PaginationResponse<TData extends ProcedureType> = {
items: TData extends { data: { items: infer Items } } ? Items : never;
count: number;
};
function usePagination<TData extends ProcedureType>(
query: UseDatatableArgs<TData>["query"]
) {
const [skip, take] = useSomethingThatManagesThis();
const response = query?.useQuery({
skip,
take,
});
return useMemo(() => ({
items: response?.data?.items,
count: response?.data?.count
}), [response]);
}