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

优化React HOC withQuery的TypeScript类型推导:自动省略注入Props

实现带自动类型推导的React Query高阶组件withQuery

核心类型设计思路

  • 从传入组件自动推导完整Props类型,通过泛型CProps捕获
  • 指定resultKey时,使用Omit工具类型自动省略该键对应的Props(该属性由查询注入,无需父组件传入)
  • 未指定resultKey时,提取useQuery查询函数返回的data类型,自动省略组件Props中匹配的属性
  • 保留手动传入泛型参数的入口,应对复杂场景下自动推导失效的情况

完整实现代码

import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
import { ComponentType, ReactElement } from 'react';

// 提取查询函数的参数类型
type QueryFnArgs<QFn extends (...args: any[]) => Promise<any>> = Parameters<QFn>;
// 提取查询函数返回的data类型(处理Promise的resolve值)
type QueryFnData<QFn extends (...args: any[]) => Promise<any>> = Awaited<ReturnType<QFn>>;

// 定义withQuery的配置项类型
type WithQueryOptions<
  CProps extends Record<string, any>,
  QFn extends (...args: any[]) => Promise<any>,
  ResultKey extends keyof CProps | undefined = undefined
> = {
  // 被包裹的目标组件
  component: ComponentType<CProps>;
  // React Query的查询函数
  queryFn: QFn;
  // 根据包裹后组件的Props生成查询函数的参数
  getQueryArgs: (props: Omit<CProps, ResultKey extends keyof CProps ? ResultKey : never>) => QueryFnArgs<QFn>;
  // 可选:指定查询结果注入到组件Props的键名
  resultKey?: ResultKey;
  // 可选:React Query的额外配置(排除queryFn和queryKey)
  queryOptions?: Omit<UseQueryOptions<QueryFnData<QFn>>, 'queryFn' | 'queryKey'>;
};

// 计算包裹后组件的最终Props类型
type WrappedComponentProps<
  CProps extends Record<string, any>,
  QFn extends (...args: any[]) => Promise<any>,
  ResultKey extends keyof CProps | undefined
> = 
  // 有resultKey时,省略该键
  ResultKey extends keyof CProps 
    ? Omit<CProps, ResultKey>
    : 
      // 无resultKey时,省略查询返回data的所有属性
      Omit<CProps, keyof QueryFnData<QFn>>;

// 实现withQuery高阶组件
function withQuery<
  CProps extends Record<string, any>,
  QFn extends (...args: any[]) => Promise<any>,
  ResultKey extends keyof CProps | undefined = undefined
>(
  options: WithQueryOptions<CProps, QFn, ResultKey>
): ComponentType<WrappedComponentProps<CProps, QFn, ResultKey>> {
  const { component: Component, queryFn, getQueryArgs, resultKey, queryOptions } = options;

  return (props: WrappedComponentProps<CProps, QFn, ResultKey>): ReactElement => {
    // 获取查询参数(类型断言确保类型兼容)
    const queryArgs = getQueryArgs(props as Omit<CProps, ResultKey extends keyof CProps ? ResultKey : never>);
    // 执行React Query查询
    const queryResult = useQuery({
      queryFn: () => queryFn(...queryArgs),
      queryKey: queryArgs, // 可根据业务需求自定义queryKey生成逻辑
      ...queryOptions,
    });

    // 组装注入到组件的Props
    const injectedProps: Partial<CProps> = resultKey
      ? { [resultKey]: queryResult.data } // 指定键名时注入到对应字段
      : queryResult.data || {}; // 未指定时展开查询结果的所有属性

    // 合并原始Props和注入的Props,传递给目标组件
    return <Component {...props} {...injectedProps} />;
  };
}

使用示例

场景1:指定resultKey注入属性

假设组件需要uuid和org两个Props,其中org由查询注入:

// 目标组件的Props类型
interface OrgCardProps {
  uuid: string;
  org: { id: string; name: string };
}

// 查询函数:根据uuid获取组织信息
const fetchOrg = async (uuid: string): Promise<{ id: string; name: string }> => {
  const res = await fetch(`/api/orgs/${uuid}`);
  return res.json();
};

// 用withQuery包裹组件
const WrappedOrgCard = withQuery({
  component: ({ uuid, org }) => (
    <div>
      <h3>{org.name}</h3>
      <p>UUID: {uuid}</p>
    </div>
  ),
  queryFn: fetchOrg,
  getQueryArgs: (props) => [props.uuid], // 这里props仅包含uuid(org已被自动省略)
  resultKey: 'org',
});

// 使用包裹后的组件:只需传入uuid
function App() {
  return <WrappedOrgCard uuid="org_12345" />;
}

场景2:未指定resultKey,自动展开查询结果

假设组件需要userId以及用户的name、email属性,查询返回完整用户对象:

interface UserProfileProps {
  userId: string;
  name: string;
  email: string;
}

// 查询函数:根据userId获取用户信息
const fetchUser = async (userId: string): Promise<{ name: string; email: string }> => {
  const res = await fetch(`/api/users/${userId}`);
  return res.json();
};

// 用withQuery包裹组件
const WrappedUserProfile = withQuery({
  component: ({ userId, name, email }) => (
    <div>
      <h3>{name}</h3>
      <p>Email: {email}</p>
      <p>User ID: {userId}</p>
    </div>
  ),
  queryFn: fetchUser,
  getQueryArgs: (props) => [props.userId], // props仅包含userId(name、email被自动省略)
});

// 使用包裹后的组件:只需传入userId
function App() {
  return <WrappedUserProfile userId="user_67890" />;
}

场景3:手动指定泛型参数(自动推导失效时)

如果遇到复杂类型导致自动推导失败,可以手动传入泛型参数:

const WrappedManualOrgCard = withQuery<
  OrgCardProps,
  typeof fetchOrg,
  'org'
>({
  component: ({ uuid, org }) => <div>{org.name}</div>,
  queryFn: fetchOrg,
  getQueryArgs: (props) => [props.uuid],
  resultKey: 'org',
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 03:25:05