skip to Main Content

I am having an interface like this. This represents the "base button".

export interface ButtonProps {
    backgroundColor?: Colors,
    children?: React.ReactNode | JSX.Element,
    style?: CSSProperties,
    disabled?: boolean,
    onClick?: () => void,
}

I now want to build ontop of that a "Text Button", a Button containing text.

interface TextButtonProps {
    buttonProps?: ButtonProps,
    textColor?: Colors,
}

I knwo want to extract the property backgroundColor from ButtonProps, using it in <TextButton />

const TextButton = ({
                        backgroundColor,
                        textColor = 'black',
                        textProps,
                        ...buttonProps,
                    }: TextButtonProps) => {
    return (
        ...
    )

Is there a way to make backgroundColor available to <TextButton />, without explicitly naming it again in the TextButtonProps? I could do it like this

interface TextButtonProps {
    backgroundColor?: Colors,
    buttonProps?: ButtonProps,
    textColor?: Colors,
}

But I would repeat myself, because ButtonProps contains already backgroundColor.

3

Answers


  1. If you want to declare a type that has additional properties, you can use an intersection:

    type TextButtonProps = ButtonProps & {
        textColor?: Colors
    }
    
    Login or Signup to reply.
  2. You could keep your types as-is and pass the properties down to Button:

    const TextButton: React.FC<TextButtonProps> = ({ textColor, buttonProps }) => (
      <Button {...buttonProps}>
        <Text color={textColor}>...</Text>
      </Text>
    );
    

    If you need to use, for example, ButtonProps.backgroundColor inside <TextButton> then you’d need to write something like this <Text foreground={textColor} background={buttonProps?.backgroundColor}>. I, personally, find TextButtonProps.buttonProps a bit confusing: think about the caller:

    <TextButton textColor="red" buttonProps={{ backgroundColor: "green" }}>
      Click me
    </TextButton>
    

    Truly confusing.

    As a note: you can pick a property from a type:

    interface TextButtonProps extends Pick<ButtonProps, "backgroundColor"> {
    }
    

    But it’s NOT what I’d do here, I’d simply extend the ButtonProps interface with the additional properties:

    interface TextButtonProps extends ButtonProps {
      textColor?: Colors;
    }
    

    Your component will then be:

    const TextButton: React.FC<TextButtonProps> = ({ children, textColor, ...buttonProps }) => (
      <Button {...buttonProps}>
        <Text color={textColor}>{children}</Text>
      </Text>
    );
    

    An extra note: you do not need to declare children if you use React.FC (I strongly suggest to always do it). Note that types for React 18 change a bit so you might need to use React.FC<React.PropsWithChildren<ButtonProps>>.
    Another one: remember that type T = { ... } & V is NOT the same as interface T extends V { ... } (even if, in most cases, you probably do not care about the difference).

    Login or Signup to reply.
  3. Try this without creating an extra type definition:

    TextButtonProps & TextButtonProps[keyof TextButtonProps] as type, it should take the nested types too

    const TextButton = ({
          backgroundColor,
          textColor = 'black',
          textProps,
          ...buttonProps,
    }: TextButtonProps & TextButtonProps[keyof TextButtonProps] ) => {
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search