4

This is my component

interface Props<C extends React.ElementType> {
  as?: C;
  children: React.ReactNode;
  size?: "mini" | "small" | "medium" | "large";
  onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}

type ButtonProps<C extends React.ElementType> = Props<C> &
  Omit<React.ComponentPropsWithRef<C>, keyof Props<C>>;

export const Button = <C extends React.ElementType = "button">({
  children,
  as,
  size = "medium",
  onClick,
  ...restProps
}: ButtonProps<C>) => {
  const Component = as || "button";

  return (
    <Component {...restProps} className={`button-${size}`} onClick={onClick}>
      {children}
    </Component>
  );
};

I can use the component like this:

<Button as="a" onClick={(e) => console.log(e)}>Anchor button</a>

The problem is that the type for the click handler says the event is of type React.MouseEvent<HTMLButtonElement, MouseEvent>, but it isn't. It should be HTMLAnchorElement in this case.

How can I provide the correct type? Ideally I'd somehow use C to get the correct HTMLXyzElement

Here's a TypeScript Playground link where you can play around with the code if you want

0

2 Answers 2

1
import React, { useRef } from "react";

type InferElement<T> =
  T extends keyof JSX.IntrinsicElements
  ? JSX.IntrinsicElements[T] extends React.DetailedHTMLProps<React.AnchorHTMLAttributes<any>, infer Elem>
  ? Elem
  : never
  : HTMLElement

type Result = InferElement<'a'> // HTMLAnchorElement

interface Props<C extends React.ElementType> {
  as?: C;
  children: React.ReactNode;
  size?: "mini" | "small" | "medium" | "large";
  onClick?: (event: React.MouseEvent<InferElement<C>, MouseEvent>) => void;
}

type ButtonProps<C extends React.ElementType> = Props<C> &
  Omit<React.ComponentPropsWithRef<C>, keyof Props<C>>;

export const Button = <C extends React.ElementType = "button">({
  children,
  as,
  size = "medium",
  onClick,
  ...restProps
}: ButtonProps<C>) => {
   const Component: React.ElementType = as || "button";


  return (
    <Component {...restProps} className={`button-${size}`} onClick={onClick}>
      {children}
    </Component>
  );
};

const Link = ({ to, children }: { to: string; children: React.ReactNode }) => {
  return <a href={to}>{children}</a>;
};

export const ButtonUser = () => {
  return (
    <div>
      <Button size="mini" disabled>
        Button text
      </Button>
      <Button as="a" size="mini" href="test" onClick={(e) => console.log(e)}>
        Button text
      </Button>
      <Button as={Link} to="/" size="mini">
        Button text
      </Button>
    </div>
  );
};

const jsx = <Button as="a" onClick={(e) => console.log(e)}>Anchor button</Button>

Since JSX.IntrinsicElements is just a big map of html elements, You can infer html element name from JSX.IntrinsicElements and use it for callback argument. see InferElement util

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

7 Comments

I see you do JSX.IntrinsicElements[T] extends React.DetailedHTMLProps<React.AnchorHTMLAttributes<any>, infer Elem>. Does this mean the util is limited to AnchorHTMLAttributes? What if I do as="button" instead of as="a"?
Please try to replace with any known html element, you will see it work
Write <Button as="div" /> and you will see that event is infered accordingly
I changed the code to just have HTMLAttributes<any> and it still seems to work. Feels a little cleaner to me :)
Where exactly you changed?
|
0

You can use HTMLElement instead of HTMLButtonElement

Comments

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.