Advanced Hooks 8 exercises

Wrapping useState Functionality with Function Overloads

Let's start by trying a naive approach.

The Naive Approach

First we'll update useStateAsObject to make the initial parameter optional.

export function useStateAsObject<T>(initial?: T) {
// ...

Now when we hover over initial, TypeScript interprets it as either T or

Loading solution


00:00 So let's take a look here. Let's try a naive approach first, where we just basically make this an optional param here. So what happens if we make it an optional param? Well, useState here ends up actually inferring it as T or undefined,

00:14 because technically initial here, it could be T or it could be undefined, which hits the first overload of useState here. So what that means then is that what happens if we just pass in this T inside there,

00:28 essentially removing the idea that this could be undefined? Well, type undefined is not assignable to this. So what we're seeing here is we're basically hitting this first overload again. You might think, okay, TypeScript is clever enough that when we hit the undefined overload, we're going to hit the second overload.

00:47 But in fact, no, whenever we pass an argument to this, it's going to actually just hit the first overload, meaning that undefined is never assignable to it. So what this means then is we need to copy useState's overloads in any function that wraps it.

01:03 So let's say then that instead of... let's actually just get the outer behavior working first, because this seems to be working. So what we're going to do is we're going to say export function useState as object. We're going to still keep the T, actually, and we're going to go initial T.

01:21 And now we end up needing actually a type here to explain what's being returned. So let's say value is T and set is going to be a React.dispatch React.setStateAction T. And this, if I remember correctly, yes, is the thing that we've got here.

01:39 So now inside our function, it's still working and outside our function, it's still working, except for just this bit here where we don't pass one. So let's do that as well. Let's actually copy this over. And now we just remove the initial T here. There we go. Looking good.

01:57 No, we're not, because this needs to be now value T or undefined. This one here too, T or undefined. If we didn't have those, you see that it's basically just behaving as it was before, where value is always a number. Whereas, in fact, we know it's not a number. You notice here, too, how easy it is for us to lie with these function overloads.

02:17 So here, we're basically never passing in undefined here. Well, rather, yeah, we're never going to pass an initial value here, but we're lying and saying that actually this value is T only, never undefined. So you have to be a bit careful with these function overloads because it's very easy to actually end up in an impossible position and TypeScript won't warn you about it.

02:38 But this one now is working beautifully. So what have we learned here, really? Well, actually, there's one more thing to do here, which is to take these object types and actually put them in a, like, this is very ugly, really. Like, look at this. So here's one I baked earlier, which is you actually can wrap these in a useState return value.

02:56 This is just a type helper that kind of sits next to these and make sure that these function definitions look a little bit prettier. And we can see, too, that we've also removed some duplication in the undefined bit, too. So we've got our T here and useState return value, T or undefined. You notice here we've got generics on or type arguments on each line.

03:15 You can actually have, like, different type arguments for different generics, too, or different, like, numbers of type argument per overload, which is extremely cool.

03:25 But yeah, like this song and dance where you have to mimic the overloads of the hook that you're using in order to get all of its API out is pretty annoying. But it's a really cool trick to need to know if you're ever doing a library that directly wraps useState or useReducer or one of the other hooks that have multiple overloads. There you go.