This probably won’t compile

How to type react-redux style HOC in Typescript

Andrej Ocenas
7 min readDec 30, 2018

Even though render props and hooks are all the rage recently, I do not think HOCs as a pattern will go away anytime soon. Typing HOCs though can be a pretty daunting task especially when you go into connect style HOC where you have a HOC factory function, that is configured with function accessing props of the wrapped component.

Obviously, the best way to find out how to do it is to look at how it was already done. Redux has a good set of declaration in DefinitelyTyped and that was exactly where I looked at. If it was easy to understand what was going on there though I would not decide to write this, so I hope this explanation will help you grasp the concepts faster.

TLDR: If you just want to see the code, here it is, otherwise, continue to step by step explanation.

First, let’s see what we want to do without any typing information in plain JavaScript

This is a simplification compared to real connect function for sake of clarity of this example. But it contains all important parts that make typing such function hard and it should be easy to see how you can build on top of it and add more functionality later on.

So, in essence, this is an injector, but a dynamic one. What it injects is dependant on the result of mapProps function. Also what mapProps function should get as an argument is dependant on the WrappedComponent because it will be its props minus whatever we are injecting.

So lets first type the mapProps function. This should be easy enough:

We do not know the exact type of neither OwnProps nor MappedProps so we need to make them generic parameters:

Again compared to react-redux typings this is fairly simple. The reason is that in react-redux you also have mapDispatchToProps, mergeProps, and options and there are overloads for various combinations of those parameters as not all are required.

So what does this first function return? First let’s figure out what is the type of the WrappedComponent. We already have OwnProps and MappedProps defined there and we know that our WrappedComponent will get both so let’s try that first:

Now it is easy to see what will be the final type as we just subtract MappedProps leaving us with OwnProps:

This kinda works but this is nowhere near the complexity of react-redux typing. So let’s see how this works and what issues there are with this simple approach.

First, it does not handle defaultProps in any way. If our WrappedComponent has some defaultProps defined, its props will be defined as non-optional but are not required when using the component. At the same time if we do not do anything special, at the moment mapProps won’t get any defaults so OwnProps are not the same as inside the WrappedComponent.

This is technically correct as the Connected component does not have any defaultProps. If you want to use num as non-optional in mapProps you need to pass it to Connected or you could define it as optional and handle it inside the mapProps.

One way to handle this is to hoist the defaultProps from WrappedComponent.

There are few important changes:

  1. We extracted the WrappedComponent type into generic parameter <C extends React.ComponentType<MappedProps & OwnProps>> so we can reuse it later.
  2. The ConnectedComponent type is <React.ComponentType<JSX.LibraryManagedAttributes<C, OwnProps>>. The JSX.LibraryManagedAttributes part is the type that handles defaultProps in a way that it makes them optional in result type.
  3. We use ConnectedComponent.defaultProps = WrappedComponent.defaultProps as any to hoist our defaults. The any part is unfortunate but necessary here. To be correct we should actually pick only defaults for OwnProps but we do not know at this time what they are. So we are assigning defaults for OwnProps + MappedProps here. A side effect of this is that we will have default values for props that are not defined in the Connected component which is not ideal.
  4. Our ConnectedComponent has props: any. This is a side effect of using C as a generic parameter instead of exact type and I am not exactly sure why. If anybody has an idea please leave a comment. In any case, you do not know what props you will get anyway so you do not usually need to have type there, you are passing it further down. If you needed some props for internal working of the HOC, you could add another type for internal props and make the ConnectedComponent require also those.

Mind that this is different from react-redux as it is not hoisting your defaultProps but it is using the JSX.LibraryManagedAttributes with a subtle issue. I do not thing hoisting the defaultProps is the best approach so I will continue without it but we will keep using the JSX.LibraryManagedAttributes type because even without hoisting the defaultProps it is needed to properly handle defaults in WrappedComponent.

The second issue this still has is that we are not actually inferring anything from the WrappedComponent. This can be ok for some use-cases but can be tedious because you need to properly define both MappedProps and OwnProps or always type the mapProps arguments when using the HOC which can be awkward when you do not need to use the props there.

Even if you do that, you created a function that takes only a special kind of WrappedComponent and so you will get type errors when wrapping the component if it does not match MappedProps and OwnProps.

This additional strictness could be seen as a personal preference but let’s try going further and figure out how to make it bit easier to use.

So first we need to figure out how to get real props of the WrappedComponent instead of always assuming it will be OwnProps + MappedProps. For that, we will use infer keyword which you can use in TypeScript although only in extend clause of a conditional type. react-redux uses a nice little helper for getting props from a ComponentType:

And we will need also a small utility type Omit:

So now we can use them instead of OwnProps in WrappedComponent definition:

WrappedComponent now extends React.ComponentType<GetProps<C>> which is kinda obvious but it means it is not strictly tied to what is defined in mapProps as OwnProps anymore.

As I mentioned, we are still using JSX.LibraryManagedAttributes here even though we are not hoisting defaultProps. The reason is that whatever is left after we remove MappedProps from WrappedComponent props needs to take defaultProps into account. At the same time, we add & OwnProps, so that we still type check what you require in mapProps and make sure it is passed into the ConnectedComponent, without taking defaultProps into account for them.

One thing that can be seen as a drawback is that MappedProps can have keys which are not expected by the WrappedComponent which in some cases can be a sign of a bug or just forgotten code but is in sync with how react-redux works.

So are we done now? What will happen if we inject the right props but of the wrong type? Right now our HOC will let that slide because we just omit our MappedProps keys without any check for the types of the keys.

In react-redux they also thought about that. They use another helper type:

The main point is that in case theDecorationTargetProps cannot accept a type of key from InjectedProps it will return type where the offending key is of type from InjectedProps. This creates a type that needs to match our WrappedComponent props otherwise we would be injecting wrong props into our component. Our generic parameter for WrappedComponent now changes to:

So at this moment, we will get a type error when we try to wrap component which cannot handle the type of injected props.

So now we are done and it looks like this.

Side note on one difference that can be spotted if you look at the react-redux typing.

The return type of the final component in react-redux is in essence this:

With Shared defined as:

This should omit only keys which are assignable to the WrappedComponent. One issue is that even though this returns type where keys that do not match types in WrappedComponent get mapped to never, when used with keyof even never keys are returned and so we remove also non-matching keys from the requirements of the final component which seems like a bug.

You could use a modified version of Shared type:

This returns shared keys which types also match directly so your final component would be:

But as we are already getting an error from our previous use of Matching type this won’t get used, because we do not allow wrapping of a component that cannot take the proper type of props. You could remove the Matching part from the WrappedComponent definition and keep Shared instead. That would mean you get an error on type mismatch at the place you are using the final ConnectedComponent, saying that you need to supply prop even though it is supplied in mapProps function (due to the mismatch in types of that injected prop). This seems to me as more confusing so I prefer the solution with Matching.

And now it is really all. Hope this helps and if you find any issue please comment and I will try to respond with something meaningful. Also, this was all done and tested with Typescript 3.2.1 so check your version first if something does not work as advertised here.

--

--