You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

关键类型推导说明

  1. PaginatableQuery:约束传入的TRPC查询必须具备接收skip/take输入、返回count/items输出的结构。
  2. inferProcedureInput<TQuery>:自动从TRPC查询中提取输入类型,确保{skip, take}符合查询的输入要求。
  3. inferProcedureOutput<TQuery>['items']:自动从TRPC查询的输出中提取items的具体类型,避免any。

修改后,Hook可完全自动推导类型,无需手动指定泛型参数,同时保持通用性。

内容的提问来源于stack exchange,提问作者maxime

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 15:52:48