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.
.@orta Do you know how to Propagate Generics across higher-order-components?
— Frederic Barthelemy (@fbartho) July 26, 2019
I have a generic component A, whose props is type Props = PubProps<B> & InjectedProps
I have abind()
-HoC that injects InjectedProps
I want return value from bind:
APrime<B> accepting PubProps<B>
The post is inspired by the problem Frederic Barthelemy tweeted about and asked me to have a look at.
React Higher Order Components (HOC) in TypeScript
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:
1 | const withLoadingIndicator = |
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.
If you’re having trouble understanding this example, check out my explanation of TypeScript generics.
Problem: Passing a Generic Component to a HOC in TypeScript
So far so good. However, let’s imagine that we have a generic component Header
:
1 | class Header<TContent> extends React.Component<HeaderProps<TContent>> {} |
…where HeaderProps
is a generic type that represents Header
‘s props given the type of associated content (TContent
):
1 | type HeaderProps<TContent> = { |
Next, let’s use withLoadingIndicator
with this Header
component.
1 | const HeaderWithLoader = withLoadingIndicator(Header); |
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: Using React’s 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.
1 | type SimpleFunctionComponent<P> = (props: P) => React.ReactElement; |
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:
1 | declare const Header: <TContent>( |
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…
1 | <TContent>(props: HeaderProps<TContent> & { isLoading: boolean }) => React.ReactElement |
…which looks very much like a generic function component!
Indeed, we can use Header
as a regular component in JSX:
1 | class Foo extends React.Component { |
Most importantly, HeaderWithLoader
is typed correctly!
Typing Higher Order Components in React - 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.
Did you like this TypeScript article? I bet you'll also like my book!
⭐️ Advanced TypeScript E-book ⭐️