Get the Props type from the component and omit the properties in it
P粉704066087
P粉704066087 2024-01-16 12:55:57
0
1
556

I'm developing a TypeScript function for a React "higher-order component". need:

  • A component,
  • React Query useQuery Type function
  • Return the parameter array and pass it to the above useQuery type function
  • Optional resultKey, 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.

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:

  1. Infer the "full" prop type (org and uuid) from the passed component
  2. Understand that because "org" is resultKey, the prop is passed in from the query and does not need to be passed in from the outside. Therefore Omitted 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?

P粉704066087
P粉704066087

reply all(1)
P粉203648742

If I understand correctly from your question, you want to infer the component type passed to withQuery and remove the property passed to the resultKey parameter from its props.

You can use the React.ComponentProps utility type to extract a component's props type. You can then use the Omit type utility to extract the properties passed into the resultKey parameter from the component's props.

type ComponentProps = React.ComponentProps
type NeededProps = Omit

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 and keyof to achieve functionality:

type KeysOfDataReturnType = keyof ReturnType['data'];
type NeededProps = Omit;
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template