Inferring Type Arguments in Curried Hooks
First we need to infer the reversedAsString
from the useComputed
hook inside of useCustomState
.
To do this, we'll add a new generic TComputed
const useCustomState = <TValue, TComputed>(initial: TValue): => {
...
Next for the useComputed
hook, we'll replace the value
Transcript
00:00 Okay, so the thing we need to think about here is that really this reversed as string needs to be inferred from somewhere. And it needs to be inferred from this useComputed bit here. The function that we pass to useComputed is going to basically dictate what this reversed as string gets inferred as,
00:18 meaning we need a generic here. Now let's say we just add a generic up the top here. We've got tComputed. Now tComputed currently is not actually being used here. Its value is never read. And if we hover over useCustomState here, it's currently being inferred as unknown.
00:36 Now what we could do here, we know probably that the value here in nums, this one shouldn't be any, it should actually be tValue here. This means then that we've got inference working properly for this set of numbers here which is being inferred from this.
00:52 If we add a boolean inside here, true, then that is going to be added into that little union there. Very, very nice. So that all looks good. But then it's this any that's getting me. Should it be tComputed? Let's give it a go. tComputed. Okay, well what's happening here then?
01:10 tComputed is still being inferred as unknown and reversed as string is now also unknown. So this is actually the wrong spot for tComputed. Now you can think of generics as being really bound to function calls or class instantiations.
01:30 This function call here, this first function call, the outer one, doesn't actually infer anything about tComputed. We can't know what tComputed is at the point this function is called. We could pass it in. We could say useCustomState. Let's say we pass in numberArray and stringArray here.
01:49 Now it all works and that's because we're sort of setting it from the outside. But what we want to do is at the moment that useComputed is called, we want to infer it there because we could potentially have multiple useComputed calls that all return different shapes of things.
02:05 So the important thing then is to remove it from here and actually put it on the useComputed function. But syntactically, how do we do that? We could kind of extract this out into a type and declare it all like that, but you can actually do it in line.
02:21 We can say tComputed just here. Isn't that pretty? So just like we've got our kind of thing on the arrow function there, we can do it here inside an object property. Now what we've got is useComputed actually becomes the source of where that gets inferred from.
02:41 And so we can basically take a look here and we can see that useComputed has got this little, I don't even know what to call this, little generic holder on it. And we can see that that type argument is being inferred in the right place. So now we can actually make another one of these if we want to.
02:56 So we can say reversedAsBoolean. And instead of string, we can just have Boolean here. And this will now be an array of Booleans and this one will stay as an array of strings. This is really, really nice.
03:09 And hopefully it's given you an idea of how functions that use generics really need to capture them at the point where they're used and where they're inferred from. This means you can come up with really amazing patterns with functions kind of like returning other generic functions, which infer the generics where they're kind of carried in.
03:28 And this is kind of called, or at least I call this pattern currying hooks, right? We're creating an outer hook and then returning a hook from inside it. It's a really beautiful way to compose behavior and I think it's underutilized.