优化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
相关产品推荐
相关产品推荐

