0

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;
}

Lin Du
  • 88,126
  • 95
  • 281
  • 483
maxime
  • 1,993
  • 3
  • 28
  • 57

1 Answers1

1

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]);
}

Peter Hassaballah
  • 1,757
  • 1
  • 9
  • 18