2
votes

I have this simple generic React component where I won't always know which element type or props I'll be using and don't want to have to set up an interface for each potential prop.

Using TS v2.9.

The fact that I'm using React Native and not React shouldn't be relevant in this instance.

import React, { StatelessComponent as SC } from "react";

interface IGenComponentProps {    
  /**
   * Component type
   */
  ComponentName: string;

  /**
   * Test Id used for automation testing
   */
  testId?: string;

  /**
   * I'd like to define this here to supress TS errors I'm getting
   */         
  remainderProps?: any;
}

const GenComponent:SC<IGenComponentProps> = ({ComponentName, testId, children, ...remainderProps}) => {

  return (
    <ComponentName id={testId} {...remainderProps}>
      {children}
    </ComponentName>
  )
}

export default GenComponent;

This works great and as expected but I get TS errors when using the component like so:

   <GenComponent 
      ComponentName={Image}
      testId="test-image"
      source={{uri: 'someImageSrc'}}
      opacity={0.1}
    />

[ts] Property 'source' does not exist on type 'IntrinsicAttributes & IGenComponentProps & { children?: ReactNode; }'.

[ts] Property 'opacity' does not exist on type 'IntrinsicAttributes & IGenComponentProps & { children?: ReactNode; }'.

or:

   <GenComponent
      ComponentName={View}
      testId="left-container"
      accessibilityHint="left-container"
    >
      { someContent }
   </GenComponent>

[ts] Property 'accessibilityHint' does not exist on type 'IntrinsicAttributes & IGenComponentProps & { children?: ReactNode; }'.

2

2 Answers

3
votes

If you want to be able to allow properties with any name without enumerating those names, you can do something like this:

interface IGenComponentProps {    
  ComponentName: string;
  testId?: string;
  [key: string]: any;
}

Be aware that you'll have limited type checking with this. Your IDE will basically enforce that ComponentName exists and is a string, and that if testId exist it is a string. Anything else is a free for all, but at least it's allowed.

0
votes

You could relax your component's props interface, but then all of your GenComponents will lose all of their children's type information.

Instead I would use a generic higher order component:

interface ITest {
  id: string;
}

function withId<T>(WrappedComponent) {
  return class extends Component<ITest & T> {
    public render() {
      return <WrappedComponent id={this.props.id} {...this.props} />;
    }
  };
}

const TestableImage = withId<ImageProps>(Image);

const ExampleUsage = props =>
  <View>
    <TestableImage
    id="required"
    source={{ uri: "also required" }}
    // Plus any other optional image image props
    />
  </View>;

In this case your wrapped component will have the props interface ITest & ImageProps

I found a good article on this pattern.