2

I ran into a very annoying problem is that I get a bunch of typescript errors, while trying to conditionally render the Next.js Link component or a button element. So if the href prop is passed, I want to render Next.js built-in Link component, else it should be Button. This is just one example of the error

Type 'HTMLAnchorElement' is missing the following properties from type 'HTMLButtonElement': disabled, form, formAction, formEnctype, and 11 more.ts(2322)

I don't know how to make typescript understand that please use (ButtonAsLinkProps & aProps) if the href prop is present, and use ButtonAsButtonProps if not.

I am very new to typescript, trying to work with it for only few weeks, pretty sure, that the types here are wrong.

This is my first post here, I am so desperate, can't find any topics related to this anywhere on the internet. Would really appreciate any help. The code for the component is bellow:

interface BaseProps {
  children: ReactNode
  primary?: boolean
  className?: string
}

type ButtonAsButtonProps = BaseProps &
  Omit<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, keyof BaseProps> & {
    href: undefined
  }

type ButtonAsLinkProps = BaseProps & LinkProps

type aProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof BaseProps>


type ButtonProps = (ButtonAsLinkProps & aProps) | ButtonAsButtonProps

const Button = ({
  children,
  className,
  href,
  primary,
  as,
  replace,
  scroll,
  shallow,
  passHref,
  prefetch,
  locale,
  legacyBehavior,
  ...rest
}: ButtonProps): JSX.Element | undefined => {
  const classNames = `${commonStyles} ${primary ? primaryStyles : secondaryStyles} ${className}`

  const linkProps = {
    as,
    replace,
    scroll,
    shallow,
    passHref,
    prefetch,
    locale,
    legacyBehavior,
  }
  if (href && linkProps) {
    return (
      <Link href={href} {...linkProps}>
        <a {...rest}>{children}</a>
      </Link>
    )
  }
  if (!href) {
    return (
      <button {...rest} className={classNames}>
        {children}
      </button>
    )
  }

}

export default Button

Denis
  • 23
  • 1
  • 4

2 Answers2

2

Here is my solution from some time ago, I could not manage to make it without some typecasting, maybe it is possible with newer version of TS.

import React, { ForwardedRef, forwardRef } from 'react';
import Link, { LinkProps } from 'next/link';

type ButtonProps = JSX.IntrinsicElements['button'] & {
  href?: undefined;
};

type AnchorProps = JSX.IntrinsicElements['a'] & {
  href: string;
} & LinkProps;

type PolymorphicProps = ButtonProps | AnchorProps;
type PolymorphicButton = {
  (props: AnchorProps): JSX.Element;
  (props: ButtonProps): JSX.Element;
};

const isAnchor = (props: PolymorphicProps): props is AnchorProps => {
  return props.href != undefined;
};

export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, PolymorphicProps>(
  (props, ref) => {
    if (isAnchor(props)) {
      const { href, as, replace, scroll, shallow, passHref, prefetch, locale, ...rest } = props;
      const linkProps = { href, as, replace, scroll, shallow, passHref, prefetch, locale };

      return (
        <Link {...linkProps}>
          <a {...rest} ref={ref as ForwardedRef<HTMLAnchorElement>} />
        </Link>
      );
    }
    return (
      <button
        {...props}
        ref={ref as ForwardedRef<HTMLButtonElement>}
        type={props.type ?? 'button'}
      />
    );
  },
) as PolymorphicButton;
// Tests
// event object will have correct type in every case
<Button href="https://google.com" onClick={(e) => e} />;
<Button type="submit" onClick={(e) => e} />;
// @ts-expect-error target is provided, but href is not
<Button target="_blank" onClick={(e) => e} />;
<Button href="https://google.com" target="_blank" onClick={(e) => e} />;
<Button href="https://google.com" target="_blank" prefetch onClick={(e) => e} />;
// @ts-expect-error Next.js prefetch is provided, but href is not
<Button prefetch onClick={(e) => e} />;

Alternatively you can remove all the logic about Next.js Link from the component and use it like that:

<Link href="https://google.com" passHref={true}>
  <Button>link text</Button>
</Link>
Danila
  • 15,606
  • 2
  • 35
  • 67
1

You can achieve something like this using CSS:

<Link className={`${isDisabled ? 'pointer-events-none' : 'cursor-pointer'}`} href={`${isDisabled ? 'http://Gogogo' : ''}`>
  My dynamic link
</Link>

If you're not using Tailwind CSS, you can create your own class and add the pointer-events: none property in your stylesheet to disable the link when necessary. Good luck!