178

I just can't wrap my head around this I guess, I've tried probably half a dozen times and always resort to any... Is there a legitimate way to start with an HTML element, wrap that in a component, and wrap that in another component such that the HTML props pass through everything? Essentially customizing the HTML element? For example, something like:

interface MyButtonProps extends React.HTMLProps<HTMLButtonElement> {}
class MyButton extends React.Component<MyButtonProps, {}> {
    render() {
        return <button/>;
    }
} 

interface MyAwesomeButtonProps extends MyButtonProps {}
class MyAwesomeButton extends React.Component<MyAwesomeButtonProps, {}> {
    render() {
        return <MyButton/>;
    }
}

Usage:

<MyAwesomeButton onClick={...}/>

Whenever I attempt this sort of composition, I get an error similar to:

Property 'ref' of foo is not assignable to target property.

6
  • 1
    You're looking for High Order Components (component factories). Check em out online and see if that fits what you're asking for. They're essentially "component factories" that will allow you to wrap a component in another component which returns that initial component, but with new or modified props. Commented Nov 21, 2016 at 23:32
  • Is the error compile time (when you compile)? Because, I tried to compile your code using tsc command and works fine. I tried to render <MyAwesomeButton onClick={() => console.log('Clicked')}/> Commented Nov 29, 2016 at 11:49
  • 2
    One thing I noticed is that, shouldn't you pass the props to your native (HTML) element as <button {...this.props} /> exactly? Commented Nov 29, 2016 at 11:53
  • 1
    This thread discusses an issue with some of the proposed answers github.com/DefinitelyTyped/DefinitelyTyped/issues/36505 and suggests interface Props extends React.ComponentProps<'button'> to capture any missing props. Commented Nov 21, 2020 at 14:28
  • 3
    I always use React.ComponentsProps<"button"> the generic input can be anything from a react component to a string such as "div". There are also the variants ComponentsPropsWithRef and ComponentsPropsWithoutRef to use when working with or without ref forwarding. Commented Jan 29, 2021 at 9:22

16 Answers 16

203

I always like to do it this way:

import React from 'react';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  title: string;
  showIcon: boolean;
}

const Button: React.FC<ButtonProps> = ({ title, showIcon, ...props }) => {
  return (
    <button {...props}>
      {title}
      {showIcon && <Icon/>}
    </button>
  );
};

Then you can do:

<Button
  title="Click me"
  onClick={() => {}} {/* You have access to the <button/> props */}
/>
Sign up to request clarification or add additional context in comments.

Comments

158

You can change the definition of your component to allow the react html button props

class MyButton extends React.Component<MyButtonProps & React.HTMLProps<HTMLButtonElement>, {}> {
    render() {
        return <button {...this.props}/>;
    }
}

That will tell the typescript compiler that you want to enter the button props along with 'MyButtonProps'

Comments

70

Seems Like the above answer is outdated.

In my case I'm wrapping a styled component with a functional component, but still want to expose regular HTML button properties.

export const Button: React.FC<ButtonProps &
  React.HTMLProps<HTMLButtonElement>> = ({
  children,
  icon,
  ...props,
}) => (
  <StyledButton {...props}>
    {icon && <i className="material-icons">{icon}</i>}
    {children}
  </StyledButton>
);

Comments

14

Most concise method

This is the method I use everytime I want to extend an HTML element.

import { JSX } from 'react';

type ButtonProps = JSX.IntrinsicElements['button']
type DivProps = JSX.IntrinsicElements['div']

3 Comments

Nice, this worked well. To import JSX, you can just do it from "react", i.e. import { JSX } from "react"
with this approach button props works good, but props.children gives error.
Nice, didn't know this one. Avoids some issues I usually had with the HTMLProps approach, and I don't seem to run into any new issues (children works fine for me, especially when going PropsWithChildren<JSX.IntrinsicElements['button']> & { ...)
12

This worked for my by using a type (instead of an interface):

type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  children: React.ReactNode;
  icon?: React.ReactNode;
};

function Button({ children, icon, ...props }: ButtonProps) {
  return (
    <button {...props}>
      {icon && <i className="icon">{icon}</i>}
      {children}
    </button>
  );
}

Comments

9

This is what I do when extending native elements:

import React, { ButtonHTMLAttributes, forwardRef } from "react";

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
    myExtraProp1: string;
    myExtraProp2: string;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
    ({ myExtraProp1, myExtraProp2, ...props }, ref) => (
        <button
            {...props}
            ref={ref}
            // Do something with the extra props
        />
    ),
);

Button.displayName = "Button";

forwardRef ensures that you can get a reference to the underlying HTML element with ref when using the component.

Comments

6
import * as React from "react";

interface Props extends React.HTMLProps<HTMLInputElement> {
  label?: string;
}

export default function FormFileComponent({ label, ...props }: Props) {
  return (
    <div>
      <label htmlFor={props?.id}></label>
      <input type="file" {...props} />
    </div>
  );
}

1 Comment

6

Extend HTML Element with Ref & Key

TL;DR
If you need to be able to accept `ref` and key then your type definition will need to use this long ugly thing:
import React, { DetailedHTMLProps, HTMLAttributes} from 'react';

DetailedHTMLProps<HTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
Type Definition
Looking at the type definition file, this is the type. I'm not sure why it isn't shorter, it seems you always pass the same HTMLElement twice?
type DetailedHTMLProps<E extends HTMLAttributes<T>, T> = ClassAttributes<T> & E;
Shortened DetailedHTMLProps

You could create your own type to shorten this for our case (which seems to be the common case).

import React, { ClassAttributes, HTMLAttributes} from 'react';

type HTMLProps<T> = ClassAttributes<T> & HTMLAttributes<T>;

export interface ButtonProps extends HTMLProps<HTMLButtonElement> {
  variant: 'contained' | 'outlined';
}
Sample Component
import React, {ClassAttributes, HTMLAttributes, ForwardedRef, forwardRef} from 'react';

type HTMLProps<T> = ClassAttributes<T> & HTMLAttributes<T>;

export interface ButtonProps extends HTMLProps<HTMLButtonElement> {
  variant: 'contained' | 'outlined';
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  (props, ref) => {

    return (
      <button key="key is accepted" ref={ref} {...props}>
        {props.children}
      </button>
    );
  },
);

1 Comment

see here why you shouldnt extend HTMLElement react-typescript-cheatsheet.netlify.app/docs/advanced/…
3

if you're using styled components from '@emotion/styled', none of the answers work.

I had to go a little deeper.

import styled from "@emotion/styled";
import React, { ButtonHTMLAttributes } from 'react';

export type ButtonVariant = 'text' | 'filled' | 'outlined';

export const ButtonElement = styled.button`
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 12px 16px;
`;

export interface ButtonProps {
  variant: ButtonVariant;
}
export const Button: React.FC<ButtonProps & React.DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>> = ({
  children,
  variant,
  ...props
}) => (
  <ButtonElement
    {...props}
  >
    {children}
  </ButtonElement>
);

this style allows you to pass all props that button has, and more than that, padding {...props} to ButtonElement allows you to easily reuse Button with styled-components, to do css changes you want in a good way

import { Button } from '@components/Button';

export const MySpecificButton = styled(Button)`
  color: white;
  background-color: green;
`;

Comments

3

You need extend your interface.

import {ButtonHTMLAttributes, ReactNode} from "react";

export interface ButtonProps extends DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>{
    appearance: 'primary' | 'ghost';
    children: ReactNode;
}

Comments

2

I solve this code for me, you just have to import ButtonHTMLAttributes from react and that's it

import { ButtonHTMLAttributes } from "react";

interface MyButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
    children: any;
}

export const MyButton = (props: ButtonI) => {
    const { children } = props;
 
    return <button {...props}>{children}</button>;
};

Comments

2
import {  FC, HTMLProps } from 'react';

const Input: FC<HTMLProps<HTMLInputElement>> = (props) => {
  return <input {...props} />;
};

Comments

0
  private yourMethod(event: React.MouseEvent<HTMLButtonElement>): void {
  event.currentTarget.disabled = true;
  }  

 <Button
 onClick={(event) => this.yourMethod(event)}
 />

Comments

0

I encountered the same issue today and here is how I fixed it:

ReactButtonProps.ts

import {
  ButtonHTMLAttributes,
  DetailedHTMLProps,
} from 'react';

/**
 * React HTML "Button" element properties.
 * Meant to be a helper when using custom buttons that should inherit native "<button>" properties.
 *
 * @example type MyButtonProps = {
 *   transparent?: boolean;
 * } & ReactButtonProps;
 */
export type ReactButtonProps = DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;

Usage in Button-ish component:

import classnames from 'classnames';
import React, { ReactNode } from 'react';
import { ReactButtonProps } from '../../types/react/ReactButtonProps';

type Props = {
  children: ReactNode;
  className?: string;
  mode?: BtnMode;
  transparent?: boolean;
} & ReactButtonProps;


const BtnCTA: React.FunctionComponent<Props> = (props: Props): JSX.Element => {
  const { children, className, mode = 'primary' as BtnMode, transparent, ...rest } = props;
  
  // Custom stuff with props

  return (
    <button
      {...rest} // This forward all given props (e.g: onClick)
      className={classnames('btn-cta', className)}
    >
      {children}
    </button>
  );
};

export default BtnCTA;

Usage:

<BtnCTA className={'test'} onClick={() => console.log('click')}>
  <FontAwesomeIcon icon="arrow-right" />
  {modChatbot?.homeButtonLabel}
</BtnCTA>

I can now use onClick because it's allowed due to extending from ReactButtonProps, and it's automatically forwarded to the DOM through the ...rest.

Comments

0

I got this to work. Somehow, extending the props by using the HtmlHTMLAttributes did not work on my end. What worked was using the ComponentPropsWithoutRef like below:

import clsx from "clsx"
import { FC } from "react";

 interface InputProps
  extends React.ComponentPropsWithoutRef<'input'>{
    className?: string;
  }

const Input: FC<InputProps> = ({ className, ...props}) => {
  return (
    <input
    className={clsx('border-solid border-gray border-2 px-6 py-2 text-lg rounded-3xl w-full', className)}
     {...props} />
  )
}

export default Input

Then I could use the usual Input props as such:

              <Input
                required
                placeholder="First Name"
                value={formState.firstName}
                className="border-solid border-gray border-2 px-6 py-2 text-lg rounded-3xl w-full"
                onChange={(e) =>
                  setFormState((s) => ({ ...s, firstName: e.target.value }))
                }
              />
   

1 Comment

If anyone has an explanation on why would be greatly appreciated.
0

Easy way for any element

interface YOUR_INTERFACE_NAME extends YOUR_ELEMENT_TYPE{
props which you want to add or use in other elements
}

EXAMPLE FOR INPUT

interface MyInput extends InputHTMLAttributes<HTMLInputElement> {
name: string;
label: string;
...
}

EXAMPLE FOR BUTTON

interface MyButton extends ButtonHTMLAttributes<HTMLButtonElement> {
name: string;
label: string;
...
}

USE SAME PATTERN FOR ALL OTHER ELEMENTS -- import those types from react or just add REACT. before them

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.