45

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
  • 753
  • 1
  • 7
  • 15

4 Answers4

90

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
}
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • 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! – Sergej Herbert Aug 14 '18 at 07:40
  • @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 Aug 14 '18 at 07:41
  • @SergejHerbert An alternative would be to use an intersection type for the props : `class Wrapper extends React.Component>` – Titian Cernicova-Dragomir Aug 14 '18 at 08:00
  • 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 ({children}); }` since a type param is required? – webbower Dec 06 '19 at 21:06
  • 1
    @webbower I've had a similar issue and for a generic HTML element, I've used: `interface WrapperProps extends React.HTMLAttributes` – Brunno Vodola Martins Feb 11 '20 at 21:57
8

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")} />;

Adam Cooper
  • 936
  • 11
  • 16
7

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.

schpet
  • 9,664
  • 6
  • 32
  • 35
2

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} />
  }
}
webbower
  • 756
  • 1
  • 7
  • 21