A photo of Fredrik Bergqvist

Fredrik Bergqvist

Web developer with over 22 years of experience. Writing about javascript and front end techniques

Typing React with typescript

TypeScript has really taken off lately in the React world and rightly so, it's a great way to keep your code documented and help you keep some errors at bay.

I have worked with TypeScript in a few projects the last few years, all involving Angular in some form but never with React so I had some new challenges in front of me when I started my current project with React and TypeScript.

Functional components

Let us start off easy with a basic functional component:

interface OwnProps {
        myProp:string;
      }
      
      const MyButton: React.FC<OwnProps> = (props) => {
        return (<button />);
      }
      
      export default MyButton as React.ComponentType<OwnProps>;

We create an interface called OwnProps where we define all props we want the component to have.

OwnProps is then used to define the component: React.FC<OwnProps> as well as when we export the component as React.ComponentType<OwnProps> to clearly signal what props are available.

In this basic example it may seem unnecessary but, as we shall see further down, when components get more complex it will save us some headache. This approach will also help negate the following error:

error TS2604: JSX element type 'MyComponent' does not have any construct or call signatures.

Wrapping another component

In some cases you might want to wrap another component and include that components interface in your own. This is usually the case when working with base components from a library and since we use Material UI (MUI for short)as a base component library I will use that in the example.

interface OwnProps {
        buttonText:string;
      }
      type Props = OwnProps & ButtonProps;
      
      const MyButton: React.FC<Props> = ({buttonText, ...rest}) => {
        return (<Button {...rest}>{buttonText}</Button>);
      };
      
      export default MyButton as React.ComponentType<Props>;

The Props type can be seen as the sum of all parts that the component will consist of. In this case we want to use ButtonProps from MUIs Button component and merge it with our own and expose both props to the consumers of the component.

This is still not very advanced but since we use MUI we also use JSS for styling so let us add that to the mix!

Using WithStyles and WithTheme

Since we use MUI we handle styling with JSS and the generated CSS classes are injected via the withStyles HOC. This caused some issues since a classes object containing the class names is injected into your props and to use classes you would need to include that object in your prop type.

Luckily we have the WithStyles type to help us! WithStyles<typeof styles> takes a generic type argument of your style object so you don't have to worry about it keeping your types DRY.

The typescript section at Material UI explains the problems with withStyles more in detail, ready up on it if you plan to use MUI and TypeScript.

const styles: (theme: Theme) => StyleRules<string> = theme =>
        createStyles({
          root: {
            margin: theme.spacing.unit
          }
        });
      interface OwnProps {
        buttonText:string;
      }
      type PublicProps = OwnProps & ButtonProps;
      type Props = PublicProps & WithStyles<typeof styles>;
      const MyButton: React.FC<Props> = ({classes, buttonText, ...rest}) => {
        return (
          <Button {...rest} className={classes.root}>
            {buttonText}
          </Button>);
      };
      export default withStyles(styles)(MyButton) as React.ComponentType<PublicProps>;

The addition we've done here is adding a PublicProps type and using that instead of the Props type when exporting the component. This is of course because we also want to use WithStyles but not expose it to anyone using the button.

Had we used the Props type instead of PublicProps we would get a pesky TypeScript error complaining about classes property missing.

TS2741: Property 'classes' is missing in type '{buttonText: string}' but required in type '{ classes: Record<string, string>}'

Redux connect and compose

But what would React be without state handling? We use Redux for this so let us connect MyButton and get the buttonText prop from state instead.

const styles: (theme: Theme) => StyleRules<string> = theme =>
        createStyles({
          root: {
            margin: theme.spacing.unit
          }
        });
      
      interface StateProps {
        buttonText:string
      }
      interface DispatchProps {
        dispatch: ThunkDispatch<IAppState, undefined, Action>;
      }
      interface OwnProps {}
      type PublicProps = OwnProps & ButtonProps;
      type Props = PublicProps & 
                   DispatchProps & 
                   StateProps & 
                   WithTheme &
                   WithStyles<typeof styles>;
      
      const MyButton: React.FC<Props> = ({classes, buttonText, ...rest}) => {
        return (
          <Button {...rest} className={classes.root}>
            {buttonText}
          </Button>);
      };
      
      const mapStateToProps = (state: IAppState): StateProps => {
        return {
          buttonText: state.buttonText
        };
      };
      
      export default compose(
        withStyles(styles, { withTheme: true }),
        connect<StateProps, DispatchProps, OwnProps, IAppState>(mapStateToProps)
      )(MyButton) as React.ComponentType<PublicProps>;

We have not started using hooks for our state so we go with the good old connect. Since we now use both connect and withStyles we need to use compose to merge them.

We create StateProps as return type of mapStateToProps and DispatchProps which types the dispatch function that is returned by default if we don't add a mapDispatchToProps function. In out case we use Thunk so if you use some other tool you need to use that instead.

I have also added an example of using the MUI theme in the component, this is done by adding { withTheme: true } as a second argument to withStyles. This will inject a theme property into your props so we need to specify that in our Props type using WithTheme from MUI.

Bonus: typing the useState hook

Not really rocket science but good to know!

const [state, setState] = useState<string>("Hello world!")`

End note

TypeScript can be very frustrating when starting out with many errors that are not clear so I hope this article could be of some help or inspiration, it took us a while to settle on a TypeScript pattern that worked for us and helped us mitigate most of the errors that might occur when working with different libraries.

Feel free to leave any suggestions in the comment field.

A gist of the code examples are available at GitHub.