Typescript HOCs with Apollo is pretty tricky. I don't know maybe it is just me but this…
This is intimidating, but it is undoubtedly necessary. How else do you want compiler to check your props in and out of the wrapped component? This is how VSCode helper describes graphql function from react-apollo. Typescript wants no harm and to protect you from yourself.
I will elaborate and extend the examples from apollo-graphql docs because they lack some use-cases like chaining HOCs or creating a query HOC with config.name, config.props.
Let's first dive into graphql the HOC creator
- TProps - Interface/Type describes so-called InputProps, notice it extends from TGraphQLVariables.
- TData - Type for the response from the query.
- TGraphQLVariables - Type for variables needed for query/mutation.
- TChildProps - This one is generated for you based on TData and TGraphQLVariables unless you want it to be customized.
DataProps are props to be generated for you from TData and TGraphQLVariables, unless you provide a custom type. This is used for the query type of action. It packs all useful for query control properties into data object.
DataValue is the one which caused me questions, mainly because it wraps TData with Partial. This design will make you check that data is not undefined in each and every consumer of HOC. To avoid it you can provide you own TChildProps.
QueryControls are props that usually go packed into data, among them there is (once the request is resolved) the prop which we typed as a response type, this is done via intersection of Partial and QueryControls.
I will not go into further detail dissecting QueryControls, because I think this information is enough to get by for the purpose of this article, in case you are prone to more exploration, feel free to clone react-apollo and dig deeper.
Let's get into the simplest query example with TS.
Following the official doc of apollographql pattern I want to fill in all the gaps, which were not obvious to me for the first couple of times when I worked with these structures, and hopefully this article will help you not go crazy, and after reading it you will get one step closer to the typescript mastery.
Query HOC without Variables
Let's first review the shortest variant possible.
withBurgersNoChildProps gets
- TProps - {}
- TData - Response
- TGraphQLVariables - {} by default
-
TChildProps - if omitted will be generated for you as Partial from the previous three types. This means data is an optional value, and you have to use non-null assertion operator - "!" everywhere. There is a way to avoid these checks.
TProps- {}
TData- Response
TGraphQLVariables- {} by default
TChildProp gets ChildProps, see how it uses ChildDataProps from react-apollo.
Let's see what is under the hood of ChildDataProps type.
ChildDataProps makes an intersection between TProps and DataProps using TData and TGraphQLVariables. Notice that there is no Partial around DataProps this time, how it was in the graphql definition example in the default value for TChildProps. This means that data is going to be definitely present in the wrapped component which is handy.
Query HOC with Variables
Here's the example of how to pass props to your wrapped component and be able to validate them.
To get the right burger, api needs a name, otherwise the request is going to fail without it. I described InputProps as an interface and extended it from Variables to avoid code duplication, it is mandatory to have Variables joined with InputProps, otherwise TS compiler will not know what props - Variables you need for your request in a graphql hoc.
Query HOC with config.options
Options is a way to map your incoming props in the HOC. For example you can map props which are named in their own way, to be treated as variable props useful for the query request.
Now there is no need to extend InputProps from Variables, because the query request is going to be satisfied with a substitution. TS also checks types inside options object declaration, so it would not let you use a property of a different type from a given Variable.
Query HOC with Options.name
The purpose of this is when you have multiple Query HOCs wrapped around one single component, data prop returned from all of them will eventually conflict, so you give every Query a specific name.
For this example, all the knowledge from above has to be put to a test. Because now we will write the result of the query into a custom specified property name. graphql function is not going to type this one for us, so we need to type the result of the query request by ourselves.
withBurgerWithName - gets the name of burgerRequest. burgerRequest will now store everything which was previously stored in data, to be able to type it we need to remember how apollo types the data prop for us. We need to mimic ChildDataPros type, here is a shortened version of it.
type ChildDataPros = TProps & { data: DataValue<TData, TGraphQLVariables> }
Notice how manually created ChildProps reflects the structure of ChildDataProps with data renamed to burgerRequest.
Chained Query HOCs
Now goes the fun part - typing the chain of HOCs. Many of you might know about compose function from apollo. In typescript it throws all your typings out the window. Here's the definition of compose function.
function compose(...funcs: Function[]): (...args: any[]) => any;
According to this, compose is a curried function. First invocation of it accepts functions, second invocation accepts anything in any quantities, the return value is any.
The aftermath
- Props passed from outside are not validated (see BurgerFactory)
- Props passed from HOC to wrapped component are not typed (has any type)
All we need to do, is to type the props in a wrapped component explicitly, which you would naturally do should you use a standalone component instead of an arrow function.
To fix the first point we just have to give up using compose with Typescript. Giving up compose leads to the most straightforward option. Let's review the example we have two HOCs prepared. One is fetching the beverage, goes by a trivial name withBeverage and the other one is our good old friend withBurger. I will show you how to jam them together.
withBurger - this time no salad, things got really serious.
withBeverage is familiar in outline but it satisfies the example.
The combination without compose will look somewhat like this
withBeverage(withBurger(MealComponent))
With the configuration of above described HOCs, the compiler will give us this error
We are interested in the first paragraph.
'ComponentClass<BurgerInputProps, any>' is not assignable to parameter of type 'ComponentType<BeverageInputProps & BeverageVariables & { beverageRequest: DataValue<BeverageResponse, BeverageVariables>; }>'
The line starting from ComponentType<BeverageInputProps & … is describing the component and its PropTypes returned after invocation of withBeverage. It conflicts with the PropTypes of the component returned from the invocation of withBurger. To fix this compiler error, we need to make intersections of the returned type from withBeverage with the incoming props type from withBurger.
First on line 2, I created an intersection of BeverageInputProps and BurgerInputProps this is needed to validate incoming props to get both requests running correctly.
On line 9, I created and intersection of BurgerInputProps & BeverageChildProps, by now you should understand that this one is put in the placeholder of TProps.
Remember earlier, the conflict of returned props from withBeverage and received props from withBurger, this way withBurger will know that it expects not only specific for a burger query variable but also some data from withBeverage.
Well that's all folks. I am thinking about doing the same explanation article about HOCs for Mutation, but I am not sure if I make it before react-apollo will release their version with hooks for everything because then everybody including me will completely forget about HOCs.
Please feel free to ask questions, give pieces of advice, suggest better approaches in the comments.
Top comments (0)