I have the following Cart.tsx code with two functions onRemove
and onAdd
with .bind()
passing some data:
const Cart = (props: CartProps) => {
// ...
const cartItemRemoveHandler = (id: string) => {
console.log("CartItemRemoveHandler");
};
const cartItemAddHandler = (item: CartItemProps) => {
console.log("CartItemAddHandler");
};
const cartItems = (
<ul className={classes["cart-items"]}>
{cartCtx.items.map((item) => (
<CartItem
key={item.id}
id={item.id}
name={item.name}
amount={item.amount}
price={item.price}
onRemove={cartItemRemoveHandler.bind(null, item.id)}
onAdd={cartItemAddHandler.bind(null, item)}
/>
))}
</ul>
);
// ...
};
CartItem.tsx:
export interface CartItemProps {
id: string;
name: string;
amount: number;
price: number;
onAdd?: (id: string) => void;
onRemove?: (item: CartItemProps) => void;
}
const CartItem = (props: CartItemProps) => {
const price = `$${props.price.toFixed(2)}`;
return (
<li className={classes["cart-item"]}>
<div>
<h2>{props.name}</h2>
<div className={classes.summary}>
<span className={classes.price}>{price}</span>
<span className={classes.amount}>x {props.amount}</span>
</div>
</div>
<div className={classes.actions}>
<button onClick={props.onRemove}>-</button>
<button onClick={props.onAdd}>+</button>
</div>
</li>
);
};
The error occurs at CartItem.tsx at the onClick
functions in the buttons. The onClick
is red underlined with the following error:
(JSX attribute) React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefined
Type '((id: string) => void) | undefined' is not assignable to type 'MouseEventHandler<HTMLButtonElement> | undefined'.
Type '((id: string) => void' is not assignable to type 'MouseEventHandler<HTMLButtonElement>'.
Types of parameters 'id' and 'event' are incompatible.
Type 'MouseEvent<HTMLButtonElement, MouseEvent>' is not assignable to type 'string'
The expected type comes from property 'onClick' which is declared here on type 'DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>'
I don't need the event properties. I just want the function to execute whenever the button is clicked. Can I work around this? Help is appreciated.
Update: Solved the error by changing the interface onAdd
and onRemove
declarations to: onAdd?: () => void;
and onRemove?: () => void;
. Essentially, I just removed the parameters from the functions in the interface, which results in the following:
export interface CartItemProps {
id: string;
name: string;
amount: number;
price: number;
onAdd?: () => void;
onRemove?: () => void;
}
The .bind()
functions are handling the arguments. There is no need to define them in the interface as well, which was my mistake.
Focussing on this line of your error chain:
Type '((id: string) => void) | undefined' is not assignable to type 'MouseEventHandler<HTMLButtonElement> | undefined'.
This error is stating that what you have configured as the type of the onAdd
hook, is incompatible with an onClick
handler that accepts a MouseEvent<...>
object as the first argument.
However, because you are binding your id
and item
inside of the Cart
element, you are actually passing functions with the type () => void
down to CartItem
instead, which is different to your interface declaration anyway. These () => void
functions are compatible with onClick
handlers because they ignore any arguments passed to them.
Therefore, you can fix the issue by updating your interface to just:
export interface CartItemProps {
id: string;
name: string;
amount: number;
price: number;
onAdd?: () => void;
onRemove?: () => void;
}
This allows you to continue using the following pieces as-is:
// Cart
onRemove={cartItemRemoveHandler.bind(null, item.id)}
onAdd={cartItemAddHandler.bind(null, item)}
// CartItem
<button onClick={props.onRemove}>-</button>
<button onClick={props.onAdd}>+</button>
However, if in the future cartItemAddHandler
or cartItemRemoveHandler
have more parameters added to them, and you don't bind all of the arguments of the function handlers properly, you will start getting MouseEvent<...>
objects passed through to your function unexpectedly.
// Cart
cartItemRemoveHandler = (id: string, quantity: number) => { ... }
/* ... */
onRemove={cartItemRemoveHandler.bind(null, item)}
// CartItem
export interface CartItemProps
/* ... */
onRemove?: () => void;
/* ... */
}
/* ... */
<button onClick={props.onRemove}>-</button>
At runtime, when onAdd
is fired, quantity
here would be given the MouseEvent<...>
, not a number
.
You can prevent mistakes like this by updating the interface to accept MouseEventHandler<HTMLButtonElement>
objects so that TypeScript appropriately reports the error when you don't bind the handlers properly.
export interface CartItemProps {
id: string;
name: string;
amount: number;
price: number;
onAdd?: MouseEventHandler<HTMLButtonElement>;
onRemove?: MouseEventHandler<HTMLButtonElement>;
}
In addition, you can swap the following lines to prevent the MouseEvent being passed in the wrong spot:
cartItemRemoveHandler.bind(null, item)
for:
() => cartItemRemoveHandler(item)
// or
(ev: MouseEvent<...>) => cartItemRemoveHandler(item) // no need to explicitly type the `ev` as a MouseEvent here, it will be inferred from the interface.
Side note: Even with these changes, the handler you use for onAdd
is accepting an item object, but onRemove
receives a string. This is backwards in comparison to your original interface.