I'm developing a TypeScript function for a React "higher-order component". need:
useQuery
Type functionuseQuery
type functionresultKey
, which determines whether query results should be propagated into components or nested under the given key. This is my implementation so far:
import React, { ComponentProps, FC } from "react"; import { UseQueryResult } from "react-query"; import { useParams } from "react-router-dom"; import { ReactQueryLoader } from "Components/Shared/Elements/ReactQueryLoader"; import { useErrorToast } from "Utils/toasts"; import { useQueryParams } from "Utils/uris"; /** The useQuery function returning the query result */ type QueryFunc = (...args: unknown[]) => UseQueryResult; /** Function returning array of args to pass to the query. Func is fed an object with URL params and passed component props. */ type GetArgsFunc<Props> = (getArgsArgs: { params: Record<string, string>; props: Props; queryParams: Record<string, unknown>; }) => unknown[]; /** The string value to pass the result under to the child component. If undefined, result is spread */ type ResultKey = string | undefined; type QueryTriplet<Props = Record<string, unknown>> = [QueryFunc, GetArgsFunc<Props>, ResultKey]; type QueryResult = Record<string, unknown> | Record<string, Record<string, unknown>>; /** * Sort of the React Query version of React Redux's `connect`. This provides a neater interface for "wrapping" a component * with the API data it requires. Until that data resolves, a loading spinner is shown. If an error hits, a toast is shown. * Once it resolves, the data is passed to the underlying component. * * This "wrapper" is a bit more complex than the typical useQuery pattern, and is mostly better for cases where you want the "main" component * to receive the data unconditionally, so it can use it in a useEffect, etc. * * @param Component The Component to be rendered once the provided query has been resolved * @param useQuery The React Query hook to be resolved and passed to the Component * @param getArgs A function returning an ordered array of args to pass to the query func. * getArgs takes an object with URL `params` and passed `props` * @param resultKey The name of the prop to pass the query data to the Component as. * If not provided, the incoming data from the query will be spread into the Component's props. * * @example * * const OrgNameContent = ({ org }: { org: CompleteOrg }) => { * const { name } = org; * return <div>Org name: {name}</div> * } * * export const OrgName = withQuery( * OrgNameContent, * useGetOrg, * ({ params }) => [params.uuid], // useGetOrg takes a single uuid param. The uuid comes from the URL. * "org" // The OrgNameContent component expects an "org" prop, so we pass the data as that prop. * ); */ export function withQuery<QueryFetchedKeys extends string = "", Props = Record<string, unknown>>( Component: FC<Props>, useQuery: QueryFunc, getArgs: GetArgsFunc<Props>, resultKey: ResultKey = undefined ) { type NeededProps = Omit<Props, QueryFetchedKeys>; const ComponentWithQuery: FC = (props: NeededProps) => { const showErrorToast = useErrorToast(); const params = useParams(); const queryParams = useQueryParams(); const queryArgs = getArgs({ params, props, queryParams }); const query = useQuery(...queryArgs) as UseQueryResult<QueryResult>; return ( <ReactQueryLoader useQueryResult={query} handleError={showErrorToast}> {({ data }) => { const resultProps = (resultKey ? { [resultKey]: data } : data) as | QueryResult | Record<string, QueryResult> as Props; return <Component {...props} {...resultProps} />; }} </ReactQueryLoader> ); }; return ComponentWithQuery as FC<NeededProps>; }
It works fine, but I'm having trouble getting the correct type. Ideally, I would pass in a component (typed) and the function would "deduce" from that component what the final set of props the component needs is. The result of calling withQuery
on that component will then be to return a component with a separate, smaller set of required props, since the withQuery
call provides no need to be passed in by the parent component props. p>
For example, if I do this:
type SomeComponentProps = { uuid: string, org: Org }; const SomeComponentBase: FC<SomeComponentProps> = ({ org }) => ( <span>{org.name}</span> ) // Would expect `uuid` as a prop, but not `org` export const SomeComponent = withQuery( SomeComponent, useGetOrg, // This query expects a uuid arg, and returns an org ({ props }) => [props.uuid], // Grab the passed uuid, and pass it in as the first and only arg to the useOrg function 'org' // Assert that the result of the query (an org), should be passed as a prop under the key "org" )
withQuery
The function should ideally be "smart" enough:
resultKey
, the prop is passed in from the query and does not need to be passed in from the outside. Therefore Omit
ted can be omitted from exported component types. Super, super ideal, if you enter useGetOrg
and no resultKey is passed (meaning the results of the query are propagated as props), the withQuery
function will be able to detect all keys of the response Provided by the query, so does not need to be passed in by the rendering parent component.
is it possible? This is a bit beyond my TypeScript capabilities at the moment.
Can you help me override this method to handle this type inference, so that the parent component only needs to pass in props that withQuery
itself does not provide?
Or, if that's not possible, maybe when you call withQuery
you could pass in the props type of the generated component?
If I understand correctly from your question, you want to infer the component type passed to
withQuery
and remove the property passed to theresultKey
parameter from its props.You can use the
React.ComponentProps
utility type to extract a component's props type. You can then use theOmit
type utility to extract the properties passed into theresultKey
parameter from the component's props.See this answer for more information on extracting React component Prop types from the component itself.
Alternatively, if you want to infer the Query's result type and remove properties from props based on that result type, you can use the
ResultType
utility type andkeyof
to achieve functionality: