Specify specific props and accept general HTML props in Typescript React App

23,620

Solution 1

We can have a look at how div props are defined:

interface IntrinsicElements {
    div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
}

If we use React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> as the base type we will have all properties of div. Since DetailedHTMLProps just adds ref to React.HTMLAttributes<HTMLDivElement> we can use just this as the base interface to get all div properties:

interface WrapperProps extends React.HTMLAttributes<HTMLDivElement> {
  callback?: Function
}

export class Wrapper extends React.Component<WrapperProps>{
  render() {
    const { callback, children, ...rest } = this.props;
    return <div {...rest}>
      {children}
    </div>;
  }
}

export const Test = () => {
  return <Wrapper className="test">Hi there</Wrapper> // works now
}

Solution 2

JSX.IntrinsicElements has this info, e.g.

const FooButton: React.FC<JSX.IntrinsicElements['button']> = props => (
  <button {...props} className={`foo ${props.className}`} />
)

// alternative...
const FooButton: React.FC<React.PropsWithoutRef<
  JSX.IntrinsicElements['button']
>> = props => <button {...props} className={`foo ${props.className}`} />

discovered this in the react-typescript-cheatsheet project.

Solution 3

Have a look at ComponentProps, ComponentPropsWithRef, and ComponentPropsWithoutRef - this will accept a generic input that can be "div", "button", or any other component. It will include react specific props such as className as well:

import React, {
  forwardRef,
  ComponentPropsWithoutRef,
  ComponentProps,
  ComponentPropsWithRef
} from "react";

const ExampleDivComponent = forwardRef<
  HTMLDivElement,
  ComponentPropsWithoutRef<"div">
>(({ children, ...props }, ref) => {
  return (
    <div {...props} ref={ref}>
      {children}
    </div>
  );
});

<ExampleDivComponent
  className=""
  style={{ background: "green" }}
  tabIndex={0}
  onTouchStart={() => alert("touched")}
/>;

const ExampleButtonComponent: React.FC<ComponentProps<"button">> = ({
  children,
  ...props
}) => {
  return <button {...props}>{children}</button>;
};

<ExampleButtonComponent onClick={() => alert("clicked")} />;

Solution 4

A co-worker of mine figured it out. Sharing here for broader visibility:

interface ComponentPropTypes = {
  elementName?: keyof JSX.IntrinsicElements; // list of all native DOM components
  ...
}


// Function component
function Component({
  elementName: Component = 'div',
  ...rest,
  // React.HTMLAttributes<HTMLOrSVGElement>) provides all possible native DOM attributes
}: ComponentPropTypes & React.HTMLAttributes<HTMLOrSVGElement>)): JSX.Element {
  return <Component {...rest} />;
}

// Class component
class Component extends React.Component<ComponentPropTypes & React.HTMLAttributes<HTMLOrSVGElement>> {
  render() {
    const {
      elementName: Component,
      ...rest,
    } = this.props;
    return <Component {...rest} />
  }
}
Share:
23,620

Related videos on Youtube

Sergej Herbert
Author by

Sergej Herbert

Updated on October 29, 2020

Comments

  • Sergej Herbert
    Sergej Herbert over 3 years

    I have a React Wrapper Component, that accepts some props, but forwards all others to the child component (especially relevent for native props like className, id, etc.).

    Typescript complains, however, when I pass native props. See error message:

    TS2339: Property 'className' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes< Wrapper > & Readonly< { children?: ReactNode; }> & Readonly< WrapperProps>'.

    How can I get a component with specific props that also accepts native props (without accepting any props and giving up on type checking)?

    My code looks like this:

    interface WrapperProps extends JSX.IntrinsicAttributes {
      callback?: Function
    }
    
    export class Wrapper extends React.Component<WrapperProps>{
      render() {
        const { callback, children, ...rest } = this.props;
        return <div {...rest}>
          {children}
        </div>;
      }
    }
    
    export const Test = () => {
      return <Wrapper className="test">Hi there</Wrapper>
    }
    

    FYI: I found a similar question here, but the answer basically gives up type checking, which I want to avoid: Link to SO-Question

  • Sergej Herbert
    Sergej Herbert almost 6 years
    Thank you very much! This seems a bit verbose. Is there no other "more react" or "more typescript" way to do that? It seems like a pretty usual use-case and extending the HTMLBaseAttributes seems a bit overkill. But the answer solves the question, so thanks!
  • Titian Cernicova-Dragomir
    Titian Cernicova-Dragomir almost 6 years
    @SergejHerbert I meant to remove the DetailedHTMLProps part, removed it now. This is the shortest most typescript way to do it as far as I know :)
  • Titian Cernicova-Dragomir
    Titian Cernicova-Dragomir almost 6 years
    @SergejHerbert An alternative would be to use an intersection type for the props : class Wrapper extends React.Component<WrapperProps & React.HTMLAttributes<HTMLDivElement>>
  • webbower
    webbower over 4 years
    How would you do accomplish this if the native element rendered by your component is determined via a prop: function MyComponent({ elementName: Component, children, ...props }) { return (<Component {...props}>{children}</Component>); } since a type param is required?
  • Brunno Vodola Martins
    Brunno Vodola Martins over 4 years
    @webbower I've had a similar issue and for a generic HTML element, I've used: interface WrapperProps extends React.HTMLAttributes<HTMLElement>