Dynamic Reducer with Generic Types
Let's work through the solution.
Since DynamicReducer
has a state
argument, we'll need at least one type argument. We can use <TState>
for that.
export class DynamicReducer<TState> {
The state
of the handler
inside of addHandler
handler will be the state that's pass
Transcript
0:00 Let's investigate the solution here. We know that dynamicReducer, because we've got this state argument here, we know that we're going to need at least one type argument that's just basically for the state. Let's pass this in here.
0:13 We've got TState. Now, TState, we can start progressing through this like it's a Sudoku actually, which of these unknowns actually correspond to the state. Well, addHandler, this.handler here, that's going to be the state. That's state.username, state.password. Let's stick that there. That's this one.
0:34 We know that's going to be TState. The handler itself has to return TState. The reduce function, too, so down here, the reducer.reduce has to take in the state and return the state. All of those, so this unknown, yeah, this one, and then this one need to be the state as well. I think that's all the places that TState needs to live.
0:59 Funnily enough, we don't need to add a property for TState. It doesn't need an attribute in the class, because it's always ephemeral actually. It's always just, "OK, we take in the state and return the new state." This state here, for instance, this is going to be TState, handler TState action. That's working good. OK-dokie.
1:18 Now, what we need to do is we need to say, we need to start typing this addHandler function, because we're getting some errors down here that's like "Object is type 'unknown,'" because addHandler currently returns unknown. All of this is unknown, blah, blah, blah, blah, blah.
1:34 On the addHandler, we take in a type and this is string basically. Let's say for now that I'm just going to leave this on types, because this is going to get rid of some of the errors down here, because now, reducer, we can see that it actually returns dynamicReducer state because we're returning this, and we're inferring the type of this from what dynamicReducer is.
1:57 Type string doesn't seem right. Now, we need to think about...This is obviously a builder pattern. Every time we call addHandler, we're returning a new version of dynamicReducer with some more type information. In this case, runtime information too.
2:14 Handlers here, we have an object, and this object is going to be as unknown. We need to work out the data structure that we want this to be in, but we know that we're going to probably need a key and then a value here.
2:32 Basically, the value here is actually going to be a handler. It's going to be this shape with this payload. I'm tempted to start this off with this, with a record -- oopsie -- string and then like this. Let's see what that does.
2:53 Maybe, I'll keep it as payload unknown for now. Now, this error goes away, because this.handlers type = handler, but we need to capture the type of TType here. I'm going to say TType extends string, and then we put in TType here.
3:10 Now, the reason I'm doing this is because we're going to need to basically say, "OK, like the handlers..." In fact, let me add an argument at the top here, which is going to be THandlers, let's say. I'm going to just default this to an empty object for now.
3:28 Because what I want to do is inside this.handler is I want to capture all the information here that in the future is going to be used to type this type LOG_OUT and all of this stuff here to basically give us the nice errors on this reducer.reduce.
3:45 We need to capture this TType in the addHandler. Let's just check that that's working. On addHandler LOG_IN, yep, we're capturing LOG_IN.
3:54 What I want to do is I also want to capture the payload that's inside here, because the payload that's passed in, this is going to be the thing that I want to...I also want to capture this when I'm building up my builder pattern.
4:09 What I'm going to do is I'm going to say payload, this is going to be TPayload. This, I think, can be...It's got to extend an object. Let's say it extends objects. That's a good way of doing that. We can say TPayload.
4:27 Now, where are we at? Now the errors go away here. This means that we're extracting out two things on this addHandler.
4:34 We're grabbing LOG_IN, and we're also grabbing username, password string. On this one, we get addHandle LOG_OUT and object. It defaults to an object when it can't find anything. That might bite us in the future, but I think that's OK for now.
4:51 Now TPayload is...Yeah, OK. This.handlers type is complaining, because this type is not assignable to this type. The reason is that payload is not...Yeah, TPayload could be instantiated with an arbitrary type, could be unrelated to unknown.
5:11 I'm going to change this to any. The reason I'm doing that is because handlers is only internal to this class. This is really the only time I interact with it actually, apart from here. I'm going to trust that this is fine, basically. That I can add any here, because it's going to be too complicated to represent this as another type.
5:34 Now, then, what I need to do is I need to say addHandler. This now, we need to figure out what it's going to return because currently...Where is it? Where is it? Where is it? Yeah, reducer now is just a dynamicReducer with state and with an empty object.
5:50 What I want this to represent is I actually want this to represent the object. I want it to represent an object where we have the keys as the types of actions and then the values as the payloads of those actions.
6:11 The way this is going to look is I'm going to return from this dynamicReducer. I'll pass in TState, and I'll pass in THandlers, and this is going to be a record with a TType, and the value is going to be TPayload.
6:31 Now, what happens? That's really nice because that's actually not complaining here. We get reducer. Whoa, here we go. Now, we have record LOG_IN, username, password, and record LOG_OUT object. Oh, that's beautiful. OK-doke.
6:48 Now then we have states = reducer.reduce. Now we have one last step here. We're actually really close. This action is typed as unknown, and we're going to take these handlers here and HandlersToDiscriminated.
7:04 Let me change that actually because this is more a payloads map, isn't it? We've got our internal handlers here, which is doing the actual runtime stuff.
7:14 The thing that we're capturing inside here, if we look, it's actually more...If we look at this, I'll just grab this out, so const...Type Example equals this and const payloads is the example.
7:33 Then it's basically asking us to produce an object like this. That's the shape it's going to be in, so LOG_OUT and this is just any object. That's the shape that's going to be captured inside our type argument.
7:48 THandlers doesn't seem like the right name for it. It should really be TPayloads. I think TPayloads, TPayloadMap, let's say so TPayloadMap and record type TPayload. That works really nicely.
8:05 Then this is going to be sort of PayloadsToDiscriminateUnion, because we saw this in the problem set up, where what this does is it takes an object like we're now producing, and it turns it into a discriminated union. This action is actually going to be PayloadsToDiscriminateUnion, TPayloadMap. OK-dokie.
8:28 Whoa. What? What are you doing here, pal? Why are you suddenly erroring? OK, that's strange. TPayloadMap does not satisfy the constraint Record, because this is actually constrained up here, so I'm going to constrain this Record, so that it's happy now and suddenly that arrow went away. That's real weird.
8:48 Now it looks like, Oh my God, all the errors are gone. Oh, my days. Now, we have our reducer, we're capturing everything inside the builder pattern beautifully. That's lovely.
9:01 Then we have our state reducer.reduce. If we try using this, we can say type LOG_IN, LOG_OUT. Yep. LOG_OUT just works. That's great. Next we have LOG_IN, and we're forced to pass in a username and password. Oh, OK. Working good.
9:18 We're getting errors here when we don't pass in the correct type and should error if you pass an incorrect payload, too. We're getting back our state.
9:28 If I try to mess things up a little bit, if I do this little bit of a screw up, then I just remove the default parameter there from the type arguments. If I remove it again...Oops, sorry about this. I'm jumping all over the place. Let me do this. Then, we're going to get errors here. This one, oh yeah, of course, it's expecting two type arguments now actually.
9:54 The error I was more intrigued by though was that this now wrecks the inference of these type arguments here, because what's happening is that this dynamic reduces state, it now defaults to Record. The default parameter is really, really crucial.
10:12 I think then, unless I've messed something up here...Yeah, type NOT_ALLOWED, pull this @ts-expect-error back in. I think that's all of it. Well done if you found this solution. This is really, really cool. We can continue to add handlers here.
10:28 Let's say we go, I don't know, update username. We pull in a function here, and this action is going to be username string. It's really nice being able to type this out here, and then we can return.
10:45 Oh, yeah, this is the first parameters, the state of course. Let's say, we return the state, and then we return username, action.username. Now, inside our reducer, we can do that ourselves. We can say type UPDATE_USERNAME, pull that in, don't need a password, blah, blah, blah, blah, blah, and it all works. Fantastic.
11:12 This was a really cool opportunity to build up a builder pattern from scratch. Well done if you managed to achieve it. No worries if you didn't. This is really hardcore stuff, but hopefully, you follow through each part of what I was discussing there. Well done.