Ad

How To Write HOC For React Apollo Query Component In TypeScript?

I'm trying to write HOC for displaying info of used Query in component like this:

const GET_RATES = gql`
  query ratesQuery {
    rates(currency: "USD") {
      currency
      rate
    }
  }
`;

class RatesQuery extends Query<{
  rates: { currency: string; rate: string }[];
}> {}

const RatesQueryWithInfo = withQueryInfo(RatesQuery);

const Rates = () => (
  <RatesQueryWithInfo query={GET_RATES}>
    {({ loading, error, data }) => {
      if (loading) return "Loading...";
      if (error || !data) return "Error!";

      return (
        <div>
          {data.rates.map(rate => (
            <div key={rate.currency}>
              {rate.currency}: {rate.rate}
            </div>
          ))}
        </div>
      );
    }}
  </RatesQueryWithInfo>
);

withQueryInfo looks like (implementation of it is based on article):

const withVerbose = <P extends object>(
  WrappedComponent: React.ComponentType<P>
) =>
  class extends React.Component<P> {
    render() {
      return (
        <div>
          {(this.props as any).query.loc.source.body}
          <WrappedComponent {...this.props as P} />;
        </div>
      );
    }
  };

This HOC works fine (it is appending query string in above original component) but the typings are broken

Error in withQueryInfo(RatesQuery)

Argument of type 'typeof RatesQuery' is not assignable to parameter of type 'ComponentType<QueryProps<{ rates: { currency: string; rate: string; }[]; }, OperationVariables>>'.
  Type 'typeof RatesQuery' is not assignable to type 'ComponentClass<QueryProps<{ rates: { currency: string; rate: string; }[]; }, OperationVariables>, any>'.
    Types of property 'propTypes' are incompatible.
      Type '{ client: Requireable<object>; children: Validator<(...args: any[]) => any>; fetchPolicy: Requireable<string>; notifyOnNetworkStatusChange: Requireable<boolean>; onCompleted: Requireable<(...args: any[]) => any>; ... 5 more ...; partialRefetch: Requireable<...>; }' is not assignable to type 'WeakValidationMap<QueryProps<{ rates: { currency: string; rate: string; }[]; }, OperationVariables>>'.
        Types of property 'fetchPolicy' are incompatible.
          Type 'Requireable<string>' is not assignable to type 'Validator<"cache-first" | "cache-and-network" | "network-only" | "cache-only" | "no-cache" | "standby" | null | undefined>'.
            Types of property '[nominalTypeHack]' are incompatible.
              Type 'string | null | undefined' is not assignable to type '"cache-first" | "cache-and-network" | "network-only" | "cache-only" | "no-cache" | "standby" | null | undefined'.
                Type 'string' is not assignable to type '"cache-first" | "cache-and-network" | "network-only" | "cache-only" | "no-cache" | "standby" | null | undefined'.ts(2345)

And also { loading, error, data } implicitly has an 'any' type.

CodeSanbox for this example is here.

How to write proper types for this HOC?

Ad

Answer

The way I read this is there is a mismatch between the declared propTypes in the Query component and QueryProps (the props for that component). The props that are wrong are are fixed below (with the original type in the comments):

export default class Query<TData = any, TVariables = OperationVariables> extends React.Component<QueryProps<TData, TVariables>> {
    static propTypes: {
        // ...
        client: PropTypes.Requireable<ApolloClient<any>>; //PropTypes.Requireable<object>;
        // ...
        fetchPolicy: PropTypes.Requireable<FetchPolicy>; //PropTypes.Requireable<string>;
        // ...
        query: PropTypes.Validator<DocumentNode>; // PropTypes.Validator<object>;
    };
}

This incompatibility does not usually matter much, except for when you try to create a HOC and use React.ComponentType<P> which validates that the propTypes and props are in agreement.

The simplest solution (baring a PR to react-apollo) is to use a weaker type for WrappedComponent, one which does not validate propTypes.

With the definition below the client code works as expected:

interface WeakComponentClass<P = {}, S = React.ComponentState> extends React.StaticLifecycle<P, S> {
  new (props: P, context?: any): React.Component<P, S>;
}

const withVerbose = <P extends any>(
  WrappedComponent: WeakComponentClass<P> | React.FunctionComponent<P>
) =>
  class extends React.Component<P> {
    render() {
      return (
        <div>
          {(this.props as any).query.loc.source.body}
          <WrappedComponent {...this.props as P} />;
        </div>
      );
    }
  };

NOTE: I hesitate to submit a PR with the corrected types as although they fix the typing issue, they do no accurately reflect the run-time validations done by propTypes. Perhaps a better approach would be to change the behavior of Validator<T> in react itself to not be co-variant, but to be contra-variant instead.

Using this definition of Validator, react-apollo works as expected:

export interface Validator<T> {
    (props: object, propName: string, componentName: string, location: string, propFullName: string): Error | null;
    [nominalTypeHack]?: (p: T) => void; // originally T, now behaves contra-variantly 
}
Ad
source: stackoverflow.com
Ad