Typescript prop discrimination in react functional component

Viewed 285

I'm creating a dynamic Widget, which can be either empty or populated. It should be by default empty. When populated={true}, I'd like the totalGross and totalNet values to be required. Otherwise, those 2 props shouldn't be allowed.

For that, I'm trying to use a discriminating type (WidgetBodyProps). However, TypeScript complains because totalNet & totalGross are required props in <WidgetBodyPopulated />

type WidgetBodyProps =
    | { isEmpty?: false; totalNet: string; totalGross: string }
    | { isEmpty: true; totalNet?: never; totalGross?: never };

type WidgetProps = WidgetHeaderProps & WidgetBodyProps;

const Widget = (props: WidgetProps) => {
    const { title = "", isEmpty = true, totalNet, totalGross } = props;

    return (
        <Card>
            <Box>
                <WidgetHeader title={title} />
                <Divider my="4" />
                {isEmpty ? (
                    <WidgetBodyEmpty flex="1" />
                ) : (
                    <WidgetBodyPopulated totalNet={totalNet} totalGross={totalGross} />
                )}
            </Box>
        </Card>
    );
};

How can I fix this Typescript Error? TS Error

2 Answers

Once you destructure the props, Typescript forgets they were ever related. So a test on one variable, will not discriminate the type of another. But at the time that you destructure props you haven't tested it's type, so all variables are typed with all possibilities.

However, if you avoid destructuring, then Typescript can keep tabs on the type of props. This way testing a preperty of props narrows the type of props are you are expecting.

{props.isEmpty ? (
    <WidgetBodyEmpty flex="1" />
) : (
    <WidgetBodyPopulated
      totalNet={props.totalNet}
      totalGross={props.totalGross}
    />
)}

Then you can even simplify those props to:

type WidgetBodyProps =
    | { isEmpty?: false; totalNet: string; totalGross: string }
    | { isEmpty: true };

Because you never access .totalNet without checking .isEmpty first.

Working example

Usually this error occurs because Typescript doesn't trust you that something isn't null. Use a non-null assertion operator to tell Typescript that it can trust that you know what you're doing and you won't feed it a null. The other option would be to ensure the variables in question are properly set to be empty strings.

Related