I have a tooltip component implemented like this (I have simplified it), which can be used in two different ways by passing a value to tooltip prop, either it will be a JSX element or a callback function that returns a ReactElement eventually:
import { isValidElement, ReactNode, useCallback, useState } from 'react';
import { Portal } from 'react-portal';
interface ChildrenProps {
onHide: () => void;
onShow: () => void;
}
type TooltipCallback = (onHide: () => void) => ReactNode;
export interface Props {
tooltip: ReactNode | TooltipCallback;
children: (props: ChildrenProps) => ReactNode;
}
export function Tooltip({ children, tooltip }: Props) {
const [show, setShow] = useState(false);
const onHide = useCallback(() => {
setShow(false);
}, []);
const onShow = useCallback(() => {
setShow(true);
}, []);
return (
<>
{children({ onHide, onShow })}
<Portal>
{show && tooltip !== null && tooltip !== undefined && (
<div className="tooltip">
{/* TS complains here */}
{isValidElement(tooltip) ? tooltip : tooltip(onHide)}
</div>
)}
</Portal>
</>
);
}
Usage by passing a callback function to tooltip:
<Tooltip
tooltip={(onHide) => (
<div>
<span>
In this tooltip, the only way to close it is by clicking the close button in the tooltip
</span>
<button onClick={onHide}>close</button>
</div>
)}
>
{({ onShow }) => <span onMouseOver={onShow}>Some text that triggers tooltip</span>}
</Tooltip>
Usage by passing a ReactElement to tooltip:
<Tooltip
tooltip={
<span>This is a tooltip that will be closed when goes away when your mouse leaves</span>
}
>
{({ onShow, onHide }) => (
<span onMouseLeave={onHide} onMouseOver={onShow}>
Some text that triggers tooltip
</span>
)}
</Tooltip>;
But typescript is complaining about it:
This expression is not callable.
Not all constituents of type 'string | number | boolean | {} | ReactNodeArray | TooltipCallback' are callable.
Type 'string' has no call signatures.ts(2349)
However, I assumed since I have the isValidElement(tooltip) check, then it is guaranteed that type of the tooltip is not string | number | {} | ReactNodeArray when trying to render the tooltip with callback function in the else clause.
How do I fix this TS error?
>Solution :
The issue with your code is that the tooltip prop is declared as type ReactNode | TooltipCallback, so TypeScript does not know that it will always be a TooltipCallback in the else clause of the conditional.
To fix this, you can add an assertion to the tooltip variable in the else clause to tell TypeScript that it is definitely a TooltipCallback at that point. You can do this by adding as TooltipCallback after the tooltip variable, like this:
{isValidElement(tooltip) ? tooltip : (tooltip as TooltipCallback)(onHide)}
With this change, TypeScript will no longer complain about the call to tooltip() in the else clause.
Here is the full code with the changes applied:
import { isValidElement, ReactNode, useCallback, useState } from 'react';
import { Portal } from 'react-portal';
interface ChildrenProps {
onHide: () => void;
onShow: () => void;
}
type TooltipCallback = (onHide: () => void) => ReactNode;
export interface Props {
tooltip: ReactNode | TooltipCallback;
children: (props: ChildrenProps) => ReactNode;
}
export function Tooltip({ children, tooltip }: Props) {
const [show, setShow] = useState(false);
const onHide = useCallback(() => {
setShow(false);
}, []);
const onShow = useCallback(() => {
setShow(true);
}, []);
return (
<>
{children({ onHide, onShow })}
<Portal>
{show && tooltip !== null && tooltip !== undefined && (
<div className="tooltip">
{/* TS no longer complains here */}
{isValidElement(tooltip) ? tooltip : (tooltip as TooltipCallback)(onHide)}
</div>
)}
</Portal>
</>
);
}