The Power of Generics and the Builder Pattern
Recapping the Problem
DynamicMiddleware class has an initial
TOutput that are inferred from what is passed in.
So we're expecting to be able to call it like this:
0:01 Conceptually, what's happening here? We have our DynamicMiddleware, and it's being called like this. We're inferring our initial TInput, our initial TOutput. When it's run, we're expecting to be able to call it with the thing that is the initial TInput, but we're expecting back the last thing that we get back from our use.
0:24 Currently, use is not generic, which is bonkers because whatever we return from the middleware that we pass to use, that's got to be the new generic inside DynamicMiddleware, because currently, inside use, this looks right but it's totally wrong because the first thing that we infer there, it then needs to be exactly the same function type throughout the rest of it, so inside here and inside here, which means that we don't get back the user on our fetchUser, and req.userId doesn't exist on type request yet.
0:59 Let's fix that. What we're going to need to do is we're going to need to add a TNewOutput here. TNewOutput, what that's going to do is it's going to be the slot where we infer what we return from that middleware. The middleware that we return is not going to be the input. It's going to be the output of the firstMiddleware.
1:21 You start to see that each one of these classes or each one of these generic slots is going to be a new line recursively going down and down and down, where the TInput stays the same, but the TOutput changes on each run. We're going to pass TOutput here.
1:38 What that means is we get request.userId. Good, we've got a little win. Now if we add a new attribute on to this, then we also get that added here. Very nice. We're getting somewhere. How are we going to infer this TNewOutput? That's going to be the thing that we get back from this use here.
2:02 We've got this use. If I were to add something onto this, let's say, so request, and let's say, wow true here, then if I hover over use, you can see that DynamicMiddleware use...Here we go, so use. Now this is the thing that's being inferred back. If I return just a number here, that will clean things up. I've got a number coming back.
2:25 You can see that use, if I get this right, there you go, it's being inferred in that slot. Now if I save this and go const middleware2 = middleware.use, then this middleware, of course, it's being inferred as any. Why is it being inferred as any? It's because use returns any as its output, because we're doing this as any. We need to figure out this return type.
2:53 You will have seen from the previous exercise, we need to return this class, but with a new set of generics in it. The way to do that, DynamicMiddleware, and this is going to be TInput, we're keeping that TInput the same, and we're changing the output, TNewOutput. Now what we have is request then, this is now middleware that takes in a request and returns a number.
3:22 We can change this. We can go takes in a request and returns a string, and go req here. This one now takes in a request and returns a user. This is just so, so cool. We can just clip that onto there, and then we've got our chains back. Now we should be getting really nice types here. We should be getting this result, user, id, firstName, lastName string, and we get autocomplete based on that.
3:58 If we stop fetching the user here, then this result.user.id no longer exists on that because it's not in the types. This middleware now takes in a request and returns a request with a user. You could use this pattern then for all sorts of stuff. This pattern is then totally generic.
4:17 We can go const middleware2, your newDynamicMiddleware. Let's say it takes in a input, which is a string, and then goes parseFloat on the string. We've got a middleware then that takes in a string and returns a number. Let's say that we go use again, and we take in the num and return num.toFixed. Now it takes in a string and returns a string.
4:46 This pattern is just so, so powerful. The keys that made it work is we have a TNewOutput that's being inferred on that use, which changes the DynamicMiddleware itself to be TInput, TNewOutput. Let's remove a couple of as anys and see why they're useful. This is, again, we're having to...We can't fix this really.
5:14 What we're saying here is we're mutating the class, and TypeScript doesn't know that we're mutating the class. We could change it totally, change the runtime implementation to return a new instance of the class, so new DynamicMiddleware, but this is just more inefficient and changes the behavior at runtime.
5:37 As any, or rather as DynamicMiddleware this, this is probably more accurate, although this doesn't even work because TNewOutput could be totally different. We say as unknown as this, which is the same as as any.
5:56 Your head is probably spinning. This is one of the trickier exercises in total TypeScript so far. I hope you can see the power here, that whenever you use one of these chainable methods or build one of these chainable methods, it gives you the potential for building these amazing chains of inference, which just can create such cool code and beautiful abstractions.
6:20You notice that I'm able to build all of this complicated stuff without a single type annotation or just one type annotation from the user. It's really, really cool. Thank you. Well done for having the gumption to go through this exercise. Nice work.