React-apollo: Mutation and TypeScript: Error when using onClick attribute with already declared interface

Created on 25 Mar 2018  ·  6Comments  ·  Source: apollographql/react-apollo

When using new <Mutation> component with TypeScript, writing onClick on the element in Mutation's render prop causes TypeScript error if the interface of the element with onClick is already declared and extends HTMLAttributes. I believe that's because of the clash of different onClick definitions. I tried to extend ButtonProps interface from both HTMLAttributes and MutationOptions but nothing changed.

Here's my code causing the error:

// Button.tsx
import React, { SFC, HTMLAttributes } from 'react';
import classNames from 'classnames';

type ButtonSize = 'small' | 'tiny';
type ButtonType = 'primary' | 'secondary' | 'danger';

interface ButtonProps extends HTMLAttributes<HTMLButtonElement> {
  size?: ButtonSize;
  type?: ButtonType;
}

const Button: SFC<ButtonProps> = ({ size, type, ...props }) => (
  <button
    className={classNames('button', [size && `button-${size}`, type && `button-${type}`])}
    {...props}
  ></button>
);

export default Button;
// OtherComponent.tsx
import React from 'react';
import Button from '../components/Button';
import { Mutation } from 'react-apollo';
import gql from 'graphql-tag';

const addClientMutation = gql`
  mutation AddClient($name: String) {
    addClient(name: $name) {
      id
    }
  }
`;

export class OtherComponent extends React.Component<any, any> {
  constructor(props: any) {
    super(props);
  }

  render() {
    return (
      <div>
        <Mutation mutation={addClientMutation} variables={{ name: 'foo' }}>
          {(addClient, { loading, error, data }) => (
            <Button type="primary" onClick={addClient}>Add New Client</Button>
          )}
        </Mutation>
      </div>
    );
  }
}

Intended outcome:
No error is thrown.

Actual outcome:
I have the following error in <Button type="primary" onClick={addClient}>Add New Client</Button>:

[ts]
Type '{ children: string; type: "primary"; onClick: (options?: MutationOptions<any, { name: string; }> ...' is not assignable to type 'IntrinsicAttributes & ButtonProps & { children?: ReactNode; }'.
  Type '{ children: string; type: "primary"; onClick: (options?: MutationOptions<any, { name: string; }> ...' is not assignable to type 'ButtonProps'.
    Types of property 'onClick' are incompatible.
      Type '(options?: MutationOptions<any, { name: string; }> | undefined) => Promise<void | FetchResult<Rec...' is not assignable to type '((event: MouseEvent<HTMLButtonElement>) => void) | undefined'.
        Type '(options?: MutationOptions<any, { name: string; }> | undefined) => Promise<void | FetchResult<Rec...' is not assignable to type '(event: MouseEvent<HTMLButtonElement>) => void'

Version

Most helpful comment

Try just wrapping the event handler in a fat arrow function:

   onClick={() => {addClient()}}

For me this makes sense as TS is properly reporting, and passing the event as addClient options is for sure not desired.

All 6 comments

you could do something like this, but you'll have to use an anonymous function:

/**
 * can replace these with Exclude in TS2.8
 * see: https://github.com/Microsoft/TypeScript/pull/21847
 */
type Diff<T extends string, U extends string> = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T]
type Omit<T, K extends keyof T> = Pick<T, Diff<keyof T, K>>

interface ButtonProps extends Omit<HTMLAttributes<HTMLButtonElement>, 'onClick'> {
  size?: ButtonSize;
  type?: ButtonType;
  onClick: MutationFn;
}

const Button: SFC<ButtonProps> = ({ size, type, onClick, ...props }) => (
  <button
    className={classNames('button', [size && `button-${size}`, type && `button-${type}`])}
    onClick={() => onClick()}
    {...props}
  ></button>
);

the following is also possible, but uses the any type.

/**
 * can replace these with Exclude in TS2.8
 * see: https://github.com/Microsoft/TypeScript/pull/21847
 */
type Diff<T extends string, U extends string> = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T]
type Omit<T, K extends keyof T> = Pick<T, Diff<keyof T, K>>

interface ButtonProps extends Omit<HTMLAttributes<HTMLButtonElement>, 'onClick'> {
  size?: ButtonSize;
  type?: ButtonType;
  onClick: MutationFn;
}

const Button: SFC<ButtonProps> = ({ size, type, onClick, ...props }) => (
  <button
    className={classNames('button', [size && `button-${size}`, type && `button-${type}`])}
    onClick={onClick as any}
    {...props}
  ></button>
);

The issue is that the button element expects the onClick to be of type (e: MouseEvent<Button>) => void but react-apollo creates a function that is <TData>(options: MutationOptions) => Promise<void | TData>.

As you can see in your error, these types are incompatible.

Try just wrapping the event handler in a fat arrow function:

   onClick={() => {addClient()}}

For me this makes sense as TS is properly reporting, and passing the event as addClient options is for sure not desired.

Ran into this exact problem today. @artola, unfortunately your solution did not work for me - I got the following TSLint error:

Lambdas are forbidden in JSX attributes due to their rendering performance impact

Since the render prop inside the <Mutation> is an SFC, it is hard to get around this error. I finally went with onClick={onClick as any} as suggested by @brettjurgens.

It sure would be nice to have a TypeScript mutation example in the examples folder. The current TypeScript example only shows a Query.

@nareshbhatia The error reported by TSLint is just a linting issue, related with the linter rules. If I would try to use onClick={onClick as any} my linter will complain too, because of the casting to any (disallowed in our company standards). That's it.

Hi @artola, totally agree - it's just a linting rule in my setup. So the solution itself works, I just have to suppress the rule for that line.

There is no intent for the mutation function to fit the typescript definition of any handler - it has it's own signature. Using a lambda or other level of indirection is the correct solution.

Was this page helpful?
0 / 5 - 0 ratings