Home > Back-end >  Exactly one/discriminated unions for react props
Exactly one/discriminated unions for react props

Time:01-11

I'm trying to define a typescript type for react component props. My component is a basic button that accepts either an icon prop, or a text prop. It can't have both, but must have one.

I was trying to start with a basic discriminated union, but it doesn't work like expected:

interface TextButtonProps extends TypedButtonProps {
  text: string
}

interface IconButtonProps extends TypedButtonProps {
  icon: JSX.Element
}

export const Button = ({ onClick, ...props }: IconButtonProps | TextButtonProps): JSX.Element => {
//...

When I use that component elsewhere, TS doesn't throw any errors:

<Button icon={<IconClose />} text='test' uiVariant='default' />

Following an article I found online, describing the interfaces with optional properties and never works:

interface TextButtonProps extends TypedButtonProps {
  text?: string
  icon?: never
}

interface IconButtonProps extends TypedButtonProps {
  icon?: JSX.Element
  text?: never
}

All of my uses of <Button> will throw an error if both icon and text exist.

Why does that work? I'm not thrilled with how verbose it is - if I add more button types I have to add those new properties to every single interface.

My second issue is that because the properties are optional, I can get away with not defining either icon or text prop - remember I need to ensure one or the other exists.

Is there a cleaner solution that would satisfy my needs?

CodePudding user response:

You don't need to define two interfaces to set the values your component will receive, one interface is enough. Declare what kind of parameters you need. in case of the icon that is the name of type string, also the caption of the button of type string; then declare the values that will not be required and initialize them in your component.

`import React from 'react';
import { Text, TouchableOpacity, StyleSheet } from 'react-native';
import { COLORS, FONTS } from '../theme/constants';

interface Props {
    title: string | JSX.Element;
    textColor?: string;
    bgColor?: string;
    onPress?: () => void;
}

const CustomButton = ({ title, textColor = COLORS.white, bgColor = COLORS.primary, onPress }: Props) => {
    return (
        <TouchableOpacity
            style={{
                ...styles.buttonLogin,
                backgroundColor: bgColor,
            }}
            onPress={onPress}
            activeOpacity={0.8}
        >
            <Text style={{
                ...styles.textButtonLogin,
                color: textColor,
                ...FONTS.body3,
            }}>
                {title}
            </Text>
        </TouchableOpacity>
    );
};

const styles = StyleSheet.create({
    buttonLogin: {
        backgroundColor: COLORS.primary,
        padding: 15,
        width: '90%',
        borderRadius: 20,
        marginTop: 30,
        alignItems: 'center',
        margin: 10,
    },

    textButtonLogin: {
        fontSize: 15,
        fontWeight: 'bold',
        color: COLORS.white,
    },
});
export default CustomButton;`

CodePudding user response:

You will have to include a common property in both interfaces, who's value is different in each of them.

interface ButtonProps {
  onClick?: (e: Event) => void;
}

interface IconButtonProps extends ButtonProps {
  type: 'icon-button';
  icon: React.ReactElement;
}

interface TextButtonProps extends ButtonProps {
  type: 'text-button';
  text: string;
}

function Button(props: IconButtonProps | TextButtonProps) {
  return null
}

export default function App() {
  return (
    <div>
      <Button type="icon-button" icon={<div />} />
      <Button type="text-button" text="text" />
    </div>
  );
}
  • Related