Refactoring a Generic Hook for Best Inference
We know that useMutation
needs to capture something specific in its type arguments.
What we're interested in is the createUser
type, which has a lot we want to capture that we can see when hovering over it:
// hovering over createUserconst createUser: (user: { name: string;
Transcript
00:00 Okey-doke, let's give this a go. We know that useMutation definitely needs to capture something in its type arguments. When we call useMutation, we want to grab the type of createUser. There's a bunch of stuff we want to grab here. We want to grab the parameters, we want to grab the return type. In fact, why don't we try grabbing
00:17 the entire function and putting it in the type argument? Let's see what happens there. What we can do is we can say tMutation and we'll pass that into tMutationOptions, because this is the point at which we actually want to grab the mutation from. While we're here, I'm actually going to rename this from mutation to mutationBase,
00:37 just so it doesn't interfere with our tMutation. Now we need to, this is erroring because, useMutationOptions is not generic, so let's give it a type arguments, let's say tMutation. Now then, this is looking pretty good. We need to then exchange the mutation,
00:55 mutationBase for tMutation. Now, this is looking good. If we hover useMutation down here, you can see that we're grabbing the entire function, so all of its arguments and all of its return values into the type argument here, so we're capturing all of it. Nice. That's good,
01:14 except that Ops.Mutation, this expression is not callable. Type unknown has no call signatures. What could that mean? Well, we know that if you don't constrain a type argument, it's going to behave as if it were unknown. We can't call unknown because we don't know that it's a function. We actually need to constrain
01:33 tMutation with mutationBase. Let's say extends mutationBase. Now, this error goes away. Nice. When I have a type argument, which we're going to pass around a lot, I like to add all the constraints in nice and early.
01:50 I know that tMutation is going to be extends mutationBase too. This is good. We're now capturing the right argument in here and we're sticking it in the right area. Great. Now, though, we're still not getting the proper errors down here. Mutation.Mutate is still typed as mutationBase,
02:09 still typed as this anything up here. We need to make sure that the thing that we're returning is the thing that we're inferring. That was a nice bit of poetry. The way we do that is we need to pass tMutation into useMutationReturn down here.
02:26 Then we need to make sure this is generic too. Let's also make sure it extends the mutationBase. Now, we put it in here instead of the mutationBase. This seems to be working. Fabulous stuff. On the outside of our function now, it's actually all doing what it's supposed to be doing.
02:45 All of our tests are passing, except we've got an error here. This is a really, truly horrible error. It exposes something wrong with our approach here, or at least there's something in our approach that I don't recommend. We have our mutate function.
03:03 This mutate function says type argsAny to promiseAny is not assignable to type tMutation. argsAny, promiseAny is assignable to the constraint of type tMutation, but tMutation could be instantiated with a different subtype of it.
03:19 What's going on here is we have our useMutationReturn here. This useMutationReturn, what it's doing is it's basically saying this mutation that comes back is exactly the same type of the mutation that we get from the thing that we pass in. tMutation is there, tMutation is there.
03:38 Lovely. Sorry, it's this one. That's the one that we're passing back. The thing is that functions are really complicated in TypeScript, because technically you can have functions that have different overloads that are different levels of complexity. You can have a function with 10 overloads
03:55 versus a function with only one overload. Because of that, TypeScript has this built-in thing where it doesn't like comparing generic functions to normal functions. This is what's biting us here. The thing that I don't recommend we do is I think we've gone
04:12 wrong in capturing the whole function in the type argument. What we should be doing instead is we should be capturing two things from this function, two different arguments. Instead of grabbing the entire thing, we actually need to grab the parameters of the function. We have user and opts here. We need to grab that in a tuple,
04:31 and we also need to grab the thing that comes back from it. We need to do a lot of refactoring. This refactoring is basically going to take the tMutation out of contention, and instead it's going to put in two type arguments instead. Let's start from the top. Let's start from here.
04:48 We're going to have tArgs and tReturn. Now use mutation options. This is also going to take tArgs and tReturn, and we are going to have to be careful with this mutation base now. You can start to see this is going to take a lot of work here.
05:06 Let's add tArgs, tReturn to all of these different slots. Let's go no more mutation for you. Let's go tArgs, tReturn. This tMutation is now no longer available, no longer in scope. What we're going to do here is we're actually going to rename
05:23 this as a mutation with some tArgs and a tReturn. Instead of these anys in here, we're going to have promise tReturn, which is the thing we get back, and then these args are going to be args, tArgs. Notice there, I didn't do tArgs like this,
05:42 but I'm using it to represent the entire array there, the entire array. Now, why is this erroring? A rest parameter must be of an array type. That's right. tArgs is unknown and we can't spread unknown. Let's do tArgs extends any array.
06:00 Now, instead of these tMutations, we're going to say mutation tArgs, tReturn. Some errors here. This type tArgs does not satisfy the constraint any. Of course, it doesn't. This is unknown, and we're trying to pass it into a slot that's extending or expect any array here.
06:20 Let's do the same here and the same up here too. Whoopsie-daisy, there we go. Something's happened. The errors have gone. We now have args is being captured as tArgs.
06:37 This result is actually being a weighted tReturn. We have our tReturn there. Now, what we're getting is everything is working. If you look in our use mutation hook, we've got now two type parameters. This is pretty hard to read, but we've got our params,
06:55 the things we're passing to our function, and we've got the thing that we're getting back from our function. These are being captured in two different parameters. This avoids the issue of having to compare functions to functions. What we're doing here is we're basically comparing now,
07:12 is this whole function is assignable to the other one because it takes in the same arguments, takes in tArgs, and it returns tReturn. This feels like such a nitpicky thing, but because we're not comparing function to function, we're actually just comparing args to args, and tReturn to tReturn,
07:31 then TypeScript is okay with that. It means, I think, that this is a best practice in general. The way you can, you should be inferring the components of functions and not the entire function itself. Just like that, we've got it working. Lovely, lovely stuff. Well done if you managed to find this solution, if this was your first solution.
07:51 If you came and went in and went through the tMutation route, like I did when I was actually trying to solve this problem first, then you'll know the pain that you might have gone through if you came across that particular error. But hopefully, this was useful.