58

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

4 Answers 4

103

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
}
Sign up to request clarification or add additional context in comments.

5 Comments

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!
@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 :)
@SergejHerbert An alternative would be to use an intersection type for the props : class Wrapper extends React.Component<WrapperProps & React.HTMLAttributes<HTMLDivElement>>
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?
@webbower I've had a similar issue and for a generic HTML element, I've used: interface WrapperProps extends React.HTMLAttributes<HTMLElement>
13

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

2 Comments

Yes, looks as ComponentProps is the proper way to go there. interface Props extends ComponentProps<'div'> {...
This approach will have a "key" in the props, which the accepted answer doesn't have. Don't know why ¯_(ツ)_/¯.
8

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.

Comments

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} />
  }
}

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.