0

Considering we have a React component that depending on different props can render either <button> or <a> or React Router <Link>

Not sure if it is even possible in this case but how can we overload this component to accept the correct props for each case?

const Button: FC<ButtonProps> = forwardRef(
  (
    {
      className,
      disabled,
      onClick,
      children,
      href,
      to,
      downloadable,
      ...rest
    },
    ref
  ) => { ... }

In addition to some shared props, unique props combination in each case is

interface BaseProps {
  bSize?: ButtonSize;
  bType?: ButtonType;
  bLayout?: ButtonLayout;
}

interface ButtonBaseProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
  ref?: Ref<HTMLButtonElement>;
}

interface ExternalLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
  onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
  ref?: Ref<HTMLAnchorElement>;
  href?: string;
  downloadable?: boolean;
}

interface ReactRouterLinkProps
  extends ForwardRefExoticComponent<LinkProps & RefAttributes<HTMLAnchorElement>> {
  onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
  ref?: Ref<HTMLAnchorElement>;
  to?: string;
}

export type ButtonProps = BaseProps & (ButtonBaseProps | ExternalLinkProps | ReactRouterLinkProps);

Normally for a function, I would do something like this

function foo(bar: string[]): string[];
function foo(bar: number[]): number[];
function foo(bar: string[] | number[]): string[] | number[] { ... }
4
  • This sounds like an XY problem. Why would you ever want a single component that does three different things? In general, this kind of "render something totally different based on props" approach is a code smell. Commented May 11, 2022 at 20:22
  • That is true. It is mostly for styling reasons. The button component also renders like a link. To keep the styles consistent, the same component is rendering all three types. I have done a similar thing with pure JS before. Of course, we can extract the styles and share them. And this is probably not worth the efforts here but for purely understanding and learning reasons, I decided to try this out anyway. The button and link is just example, there could be a real usecase for the problem though. Commented May 11, 2022 at 20:26
  • How are you managing your styles? But yeah, extracting and sharing styles is the way to go here. There's no elegant solution, you'd just have to take props as a union of the prop types and have a nasty if-else Commented May 11, 2022 at 20:30
  • 1
    It's TailwindCSS, and can easily extract them out. Would be cleaner. if...else is what I am doing. As I said, will leave the question open anyway for learning something. Commented May 11, 2022 at 20:35

1 Answer 1

1

What you did is correct, you need just one thing, you need to add an additional prop type to help TypeScript to know which type you are using in your code:

import * as React from 'react';
import {LinkProps} from 'react-router-dom';
import './style.css';

export default function App() {
  return (
    <CustomComponent type='anchor' bSize='big' />
  );
}

interface CommonProps {  
  bSize?: any;
  bType?: any;
  bLayout?: any;
}

interface ButtonProps extends React.HTMLProps<HTMLButtonElement>  {
  type: 'button'
}

interface  AnchorProps extends React.HTMLProps<HTMLAnchorElement>  {
  type: 'anchor'
}

interface CustomLinkProps extends LinkProps {
  type: 'link'
}

type Props = (ButtonProps | AnchorProps | CustomLinkProps) & CommonProps

function CustomComponent (props: Props) {
  return <div></div>
}

And don't forget to use Discriminating Unions, check this: https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html

Demo: https://stackblitz.com/edit/react-ts-3yfbmt?file=App.tsx

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks, @soufiane, Discriminating Unions was the key. However, the solution is still not complete because of the forwardRef here. Still getting an incompatible ref error.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.