DEV Community

Cover image for Typing Higher Order Components in React
Milosz Piechocki
Milosz Piechocki

Posted on • Updated on • Originally published at codewithstyle.info

Typing Higher Order Components in React

Some time ago I wrote about generic type arguments propagation feature added in TypeScript version 3.4. I explained how this improvement makes point-free style programming possible in TypeScript.

As it turns out, there are more cases in which propagation of generic type arguments is desirable. One of them is passing a generic component to a Higher Order Component in React.

The post is inspired by the problem Frederic Barthelemy tweeted about and asked me to have a look at.

Higher Order Components

I'm not going to give a detailed explanation, as there are already plenty to be found on the internet. Higher Order Component (HOC) is a concept of the React framework that lets you abstract cross-cutting functionality and provide it to multiple components.

Technically, HOC is a function that takes a component and returns another component. It usually augments the source component with some behavior or provides some properties required by the source component.

Here is an example of a HOC in TypeScript:

const withLoadingIndicator = 
    <P extends {}>(Component: ComponentType<P>): ComponentType<P & { isLoading: boolean }> => 
        ({ isLoading, ...props }) =>
            isLoading 
                ? <span>Loading...</span> 
                : <Component {...props as P} />;
Enter fullscreen mode Exit fullscreen mode

As you can deduce from the type signature, withLoadingIndicator is a function that accepts a component with P-shaped properties and returns a component that additionally has isLoading property. It adds the behavior of displaying loading indicator based on isLoading property.

HOC type

Problem: passing a generic component to a HOC

So far so good. However, let's imagine that we have a generic component Header:

class Header<TContent> extends React.Component<HeaderProps<TContent>> { }
Enter fullscreen mode Exit fullscreen mode

...where HeaderProps is a generic type that represents Header's props given the type of associated content (TContent):

type HeaderProps<TContent> = {
    content: TContent;
    title: string;
}
Enter fullscreen mode Exit fullscreen mode

Next, let's use withLoadingIndicator with this Header component.

const HeaderWithLoader = withLoadingIndicator(Header);
Enter fullscreen mode Exit fullscreen mode

The question is, what is the inferred type of HeaderWithLoader? Unfortunately, it's React.ComponentType<HeaderProps<unknown> & { isLoading: boolean; }> in TypeScript 3.4 and later or React.ComponentType<HeaderProps<{}> & { isLoading: boolean; }> in previous versions.

As you can see, HeaderWithLoader is not a generic component. In other words, generic type argument of Header was not propagated. Wait... doesn't TypeScript 3.4 introduce generic type argument propagation?

Solution: use function components!

Actually, it does. However, it only works for functions. Header is a generic class, not a generic function. Therefore, the improvement introduced in TypeScript 3.4 doesn't apply here ☹️

Fortunately, we have function components in React. We can make type argument propagation work if we limit withLoadingIndicator to only work with function components.

Unfortunately, we cannot use FunctionComponent type since it is defined as an interface, not a function type. However, a function component is nothing else but a generic function that takes props and returns React.ReactElement. Let's define our own type representing function components.

type SimpleFunctionComponent<P> = (props: P) => React.ReactElement;

declare const withLoadingIndicator: 
    <P>(Component: SimpleFunctionComponent<P>) => 
        (SimpleFunctionComponent<P & { isLoading: boolean }>);
Enter fullscreen mode Exit fullscreen mode

By using SimpleFunctionComponent instead of FunctionComponent we loose access to properties such as defaultProps, propTypes, etc., which we don't need anyway.

Obviously, we need to change Header to be a function component, not a class component:

declare const Header: <TContent>(props: HeaderProps<TContent>) => React.ReactElement;
Enter fullscreen mode Exit fullscreen mode

We wouldn't be able to use FunctionComponent here anyway, since Header is a generic component.

Let's now take a look at the inferred type of HeaderWithLoader. It's...

<TContent>(props: HeaderProps<TContent> & { isLoading: boolean }) => React.ReactElement
Enter fullscreen mode Exit fullscreen mode

...which looks very much like a generic function component!

Indeed, we can use Header as a regular component in JSX:

class Foo extends React.Component {
    render() {
        return (
            <HeaderWithLoader 
                title="Hello" 
                content={12345} 
                isLoading={false} />
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Most importantly, HeaderWithLoader is typed correctly!

Summary

As you can see, typing HOCs in React can get tricky. The proposed solution is really a workaround - ideally, TypeScript should be able to propagate generic type arguments for all generic types (not only functions).

Anyway, this example demonstrates how important it is to stay on top of the features introduced in new TypeScript releases. Before version 3.4, it wouldn't be even possible to get this HOC typed correctly.

Want to learn more?

Did you like this TypeScript article? I bet you'll also like my book!

⭐️ Advanced TypeScript ⭐️

Top comments (0)