Home > Net >  Interface for a React Button component that can be an anchor tag or a button tag
Interface for a React Button component that can be an anchor tag or a button tag

Time:09-06

I'm trying to create a button component that can be an anchor tag with a href or a button tag with a type prop. I have the following code:

interface IBaseProps {
  children: string;
  fullSize?: boolean;
  theme?: 'primary' | 'secondary' | 'dark';
}

interface ILinkButtonProps extends IBaseProps {
  url: string;
  type: never;
  props?: AnchorHTMLAttributes<HTMLAnchorElement>;
}

interface IButtonProps extends IBaseProps {
  type: 'button' | 'submit' | 'reset';
  url: never;
  props?: ButtonHTMLAttributes<HTMLButtonElement>;
}


export const Button = ({
  children,
  props,
  theme = 'primary',
  fullSize = false,
  type = 'button',
  url,
}: IButtonProps | ILinkButtonProps): JSX.Element => {
  const Tag:  keyof JSX.IntrinsicElements = url ? 'button' : 'a';

  return (
    <Tag
      className={`${styles.button} ${styles[theme]} ${
        fullSize ? styles.fullSize : ''
      }`} // not important
      
      {...props}
      {...(Tag === "button" ? {type: `${type}`} : {href: url})}
    >
      {children}
    </Tag>
  );
};

However, that gives me some typing errors, for instance:

Types of property 'onCopy' are incompatible. Type 'ClipboardEventHandler | undefined' is not assignable to type 'ClipboardEventHandler | undefined'. Type 'ClipboardEventHandler' is not assignable to type 'ClipboardEventHandler'. Type 'HTMLAnchorElement' is missing the following properties from type 'HTMLButtonElement': disabled, form, formAction, formEnctype, and 11 more.

Is there a way for me to format my code with typescript, so I can achieve a component that allows me to have both a button or a link?

CodePudding user response:

Having the separate Tag variable is your downfall. Typescript is not intelligent enough to "see through" this variable and use it as a union discriminator. Once you assign to to a diff variable, the context of its meaning is lost.

You have to be quite explicit and pass the whole props into your new type guard, then consume that immediately after without assigning flags that you use later.

You will need strict null checks on for this to work. Heres a code sandbox https://codesandbox.io/s/heuristic-haslett-ovqkcw

import React from "react";

type IBaseProps = {
  children: string;
  fullSize?: boolean;
  theme?: "primary" | "secondary" | "dark";
};

type ILinkButtonProps = IBaseProps & {
  url: string;
  type?: never;
  props?: React.AnchorHTMLAttributes<HTMLAnchorElement>;
};

type IButtonProps = IBaseProps & {
  type: "button" | "submit" | "reset";
  url?: never;
  props?: React.ButtonHTMLAttributes<HTMLButtonElement>;
};

export const Button = (props: ILinkButtonProps | IButtonProps): JSX.Element => {
  const { children, theme, fullSize, type, url } = props;

  const commonProps = {
    className: `${styles.button} ${styles[theme]} ${
      fullSize ? styles.fullSize : ""
    }`
  };

  if (props.type) {
    return (
      <button {...commonProps} type={type} {...props.props}>
        {children}
      </button>
    );
  }

  return (
    <a {...commonProps} href={url} {...props.props}>
      {children}
    </a>
  );
};

BTW, in my opinion, you shouldn't do what you are trying to do. Whilst its possible, this component likely breaks the principle of element of least surprise for the developer. Links and buttons are not semantically the same -- the dev should really make a very active choice.

  • Related