React Query + TRPC通用分页Hook的TypeScript类型定义问题
通用TRPC分页Hook的TypeScript类型定义问题
我需要编写一个通用Hook,接收TRPC的useQuery作为参数来处理分页逻辑。该查询固定接收skip和take参数,响应固定包含count字段和items数组。
原生JavaScript可行实现
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; }
TypeScript类型定义失败的尝试
尝试复用TRPC客户端类型时,无法区分请求类型与响应类型,导致返回的paginated.items类型为any:
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]); }
在组件中使用时,类型丢失:
function MyComponent() { const paginated = usePagination(api.user.list); const firstUserEmail = paginated.items?.[0].email; // paginated.items 类型为 any }
临时解决方案(手动指定泛型)
目前通过TRPC的路由输出类型推断作为临时方案,但希望能直接从传入的查询自动推导类型:
export type RouterOutputs = inferRouterOutputs<AppRouter>; function MyComponent() { const paginated = usePagination<RouterOutputs["user"]["list"]>(api.user.list); const firstUserEmail = paginated.items?.[0].email; }
正确的TypeScript类型实现
要让Hook自动从传入的TRPC查询中推导类型,需要利用TRPC提供的inferProcedureInput和inferProcedureOutput工具类型,同时约束查询的输入必须包含skip和take,输出必须包含count和items:
import { useMemo } from 'react'; import { AnyQueryProcedure, inferProcedureInput, inferProcedureOutput } from '@trpc/server'; import { UseQueryResult } from '@tanstack/react-query'; // 定义分页查询的输入约束 type PaginationInput = { skip: number; take: number }; // 定义分页查询的输出约束 type PaginationOutput<TItems> = { count: number; items: TItems[] }; // 约束传入的TRPC查询必须符合分页输入输出格式 type PaginatableQuery = AnyQueryProcedure & { useQuery: (input: inferProcedureInput<AnyQueryProcedure>) => UseQueryResult<PaginationOutput<any>>; }; // 优化后的通用分页Hook function usePagination<TQuery extends PaginatableQuery>(query: TQuery) { const [skip, take] = useSomethingThatManagesThis(); const response = query.useQuery({ skip, take } as inferProcedureInput<TQuery>); return useMemo(() => ({ items: response.data?.items as inferProcedureOutput<TQuery>['items'], count: response.data?.count, isLoading: response.isLoading, error: response.error, }), [response]); } // 使用示例(无需手动指定泛型) function MyComponent() { const paginated = usePagination(api.user.list); // paginated.items 自动推导为 User[],可正常访问email属性 const firstUserEmail = paginated.items?.[0].email; }
关键类型推导说明
PaginatableQuery:约束传入的TRPC查询必须具备接收skip/take输入、返回count/items输出的结构。inferProcedureInput<TQuery>:自动从TRPC查询中提取输入类型,确保{skip, take}符合查询的输入要求。inferProcedureOutput<TQuery>['items']:自动从TRPC查询的输出中提取items的具体类型,避免any。
修改后,Hook可完全自动推导类型,无需手动指定泛型参数,同时保持通用性。
内容的提问来源于stack exchange,提问作者maxime
相关产品推荐
相关产品推荐

