Understanding Generics at Different Levels of Functions
This challenge can be solved by only changing the clone
line, so let's start by really understanding what's happening there.
Here's what our starting point looked like:
export interface Cache<T> { get: (key: string) => T | undefined; set: (key: string, value: T) => void; // Y
Transcript
0:00 This is an interesting solution. We can fix it by only changing the line below, but we need to know really what is the line below doing. It's declaring a type definition for the clone function, which lives down here.
0:15 These createCache here, it has a return type of Cache. The thing that's down here, it basically needs to match up to this Cache thing here. Basically, all the type definitions are happening really up here. Well, sorry. Except for the ones that declare this as a generic function and then return the Cache there.
0:35 We've got our clone here. This transform function, it's basically going to take an element through the process of transformation. Its input is going to be the thing that's in the Cache because it's run on each Cache member.
0:51 We're now getting elem defined as a number because this is the initialCache setting that we've got there. We've got our transform. Then we need to change it to something. The thing that this function returns is going to be the thing that's in the new Cache. Whatever this is, it's going to be both here and in the Cache.
1:13 What we could do is define U up here, but then we've got some issues actually. We've got some issues all over the place. Because U then, it would actually need to be defined the first. It just feels like the wrong moment to make that definition. When we first return the Cache, we also then need to say what the Cache is going to transform into? No. That doesn't seem right.
1:36 There must be another place that we can put that U. Turns out there is. We can put it right here, on the clone function. Suddenly, everything goes away, which is cool. We've got our U here. It's being inferred when clone is actually called. When we call this clone here, you can see that string is being locked in there.
1:59 If I were to change this to something else, like elem > 2, like a boolean or 1/. In fact, I can just say, "elem > 2." What we end up with then is a boolean Cache instead of a number Cache. If you hover over this, this function then is returning a boolean. Let's return it to its former stringified glory.
2:25 This function down here, this clone, this is cool too. We can see here that if you hover over it on the clone, then you're getting this U even though we're not actually declaring it here. We can actually pull out U if we want to.
2:40 You can see here that transform implicitly has an any type. Because we've started giving it type definitions, we need to really finish. This isn't necessarily what I would recommend doing, but it is useful to see what this is doing here.
2:57 We've got our U there. We've got transform elem T into U. Now we can actually replace that any with a U there because that is what's happening there. You can see the transform is taking our elem from T, which is cache key, into U, which is pretty nice.
3:15 To be honest, I would probably just keep this as any, keep this as the base transform thing here, and then just take that and move it out. It's pretty nice just having all of this stuff available in this Cache thing here.
3:30 The key lesson is that even if you're inside an interface or even if you're inside a type, let's say, then you could actually add a function which has a generic on it right there, super-duper useful and especially useful -- this pattern is so, so common -- when you pass in a function to transform something to something else.
3:51 You can even clone it again if you want to. You can even say, "elem," let's say. Let's say, "return elem" and have blah, blah, blah, blah, blah, blah at the end. Then it's still could be a string Cache. If we return it back to a number, if you want to, like parseFloat elem, let's say, then we're going to get a number Cache at the end.
4:12 This pattern, every time we're calling clone now, we're basically adding a new generic into the pile, just based on what's being passed here and, again, with no type annotations whatsoever. So, so cool. This is the basis of the builder pattern of chaining methods that we'll see in future exercises.