Home > Enterprise >  Typescript React `isValidElement` type-guard not working as expected
Typescript React `isValidElement` type-guard not working as expected

Time:12-03

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 onm ouseOver={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 onm ouseLeave={onHide} onm ouseOver={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?

CodePudding user response:

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>
        </>
    );
}
  • Related