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
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.
Most helpful comment
Try just wrapping the event handler in a fat arrow function:
For me this makes sense as
TSis properly reporting, and passing theeventasaddClientoptions is for sure not desired.