Advanced Patterns 11 exercises

Add Generic Component Support to a Higher Order Component

Before we start solving this problem, let's recap what we learned from working with forwardRef.

We know that the type definition for ComponentType has a couple of extra things on it:

interface FunctionComponent<P = {}> {
(props: P, context?: any): ReactNode;
propTypes?: WeakV

Loading solution


00:00 Let's get started by using what we learned from forward ref, which is that we know that this component type has a couple of extra things on it, which is going to make it a little dodgy. This props p context t, it's going to lose the generosity because of this type here.

00:19 If we take this back to a bare metal type, then we can say react.reactNode here instead, and grab the props and stick it on tprops here. Now, I was solving this myself actually, in a previous take, actually, take me behind the curtain. What I noticed here was actually like you get

00:38 this really strange error here where with router doesn't seem to be able to understand the type that's being inferred here. That's because you notice that the type that we're returning has still got a display name on it. It's got props,

00:55 omit unknown router JSX element, and it's got this display name here. Now, why on earth would this be happening here? Because we're not taking in a component with display name, but display name seems to be being added here. Well, that's because actually, on the return type of our function here,

01:13 it's not actually like it's not annotated, so it's attempting to infer a return type. One amazing thing about TypeScript, and a slightly strange property of it, is that with functions, you can actually freely annotate or add properties to them in certain instances. Here, we're actually modifying

01:32 new component so that it's got a display name attribute. If we comment that out, the new component is just a bare function itself, and actually, this works now. This is extremely strange. This is so when we're working just with a normal function here,

01:50 you can see that there's no more objects syntax there. If I uncomment this and go back here, you can see that we go back to this funky little object syntax and display name is added. But if I comment it out, then it's no longer there, and we just return a bare function, and so TypeScript can perform

02:08 a higher order function operations on it. Actually, this is enough to make it pass, except we want this line of code in here. We need this line of code to happen at runtime. The way to get around this is to actually add in a type annotation with

02:27 router to basically make it work on the type level, because we don't want it to infer here, because actually it's inferring an unhelpful thing from our line of code, and we've still got an error here. Let's actually add a return type here. The return type we're going to add is we're actually going to

02:44 add it as just a bare function like this to make it match up. We have props returns React.ReactNode. We need to make sure that this annotation here has exactly the same signature as this one here. Because if we just add tprops,

03:02 then it's actually not going to be assignable here, because if we look down the bottom here, then actually this still expects a router to be passed in, whereas we actually want to omit the router from there. Let's say omit tprops router, and that will work nicely. Now, everything's passing here, even though our components inside

03:21 is still moaning a little bit. We need to finally figure out this little error here. Property displayName does not exist on the type that we're passing in here. This is by design, because if we add this here, if we say it's actually a function that has

03:40 this call signature and has displayName string here, then of course, all of our operations down the bottom are going to stop working. We lose the generosity of the function. We're stuck here. We need somehow to make sure the displayName is on the thing that gets passed in,

03:58 but we don't always need it. Well, I think the best thing to do here is just to add an as. This component is as displayName string, and this makes everything happy. If I just return this to the type that it was,

04:15 then we should have a working solution. This is my solution here, is to basically say, with router, still a generic function, add a specific return type so that this curious displayName mutation doesn't leak out into the implementation and ruin the inference. Then also inside here,

04:35 make sure that displayName is just casted to a type that makes sense, really. This is a proper way to get it all working, and it means that you can use with router and HOCs with generic components.