Challenges 5 exercises
solution

Narrowing with Arrays and Generics

First, let's capture the fruit being passed in using a TFruits generic type argument on the wrapFruit function:


export const wrapFruit = <TFruits>(fruits: TFruits) => { const getFruit = (name: unknown) => { return fruits.find((fruit) => fruit.name === name); }; return { getFruit,

Loading solution

Transcript

0:00 Let's do the things that we know we need to do first. We need to capture the fruit that's being passed in so that this identity function actually does something. Let's start by adding a type argument on the top function here on wrapFruit itself and saying TFruits. Let's just stick it inside here.

0:17 Of course, this is not going to do very much because we're capturing the basic stuff inside here, but we need to add a const annotation to this in order to make it actually capture the literal values, which are going to be important later. We know we need to capture apple because we want this to have autocomplete down there. That's going to need to be in there.

0:37 We currently don't know that we have a find property on TFruits because this could be called with anything. We need to constrain it. This needs to be extends Fruit array here. Except, of course, this means that wrapFruit here is actually being inferred as Fruit array, not its literal values. We've been caught out by this before. We can say readonly Fruit inside here.

1:01 Readonly Fruit now means that this array gets marked as readonly, meaning it's available to be consted. It's available to be inferred deeply as its literals. We end up with readonly, readonly, readonly everywhere and everything's being inferred as its literals, which is great.

1:18 Now the next thing I'm looking at is this name function here or name parameter, because we can see that it's being typed as name unknown, meaning we can pass any old thing to this, which is not good. Let's just annotate that first. We can say TFruits number and name.

1:35 What this is doing then is it's saying TFruits number name. We're basically turning those TFruits into a union and then extracting out the discriminator, which in this case is the name. We know that every single thing has a name here. I wonder if I can...No, it doesn't matter too much. This means that we're just basically getting a union out of the names.

1:56 Now we can see that banana and apple are added there. If we add another one here, let's say add orange, then that's going to add itself to the union as well. Let's say price 3, just to differentiate it, and getFruit orange. Beautiful stuff.

2:10 Now this is OK, except for the fact that this return type is Fruit or undefined. We want it to be the specific fruit that's coming from this type being inferred here, except that getFruit here, name apple, banana, or orange, let me just remove orange for clarity, this here is not being actually inferred as the member, it's being inferred as the union of all possible members.

2:36 We've got apple or banana there. This isn't so good. We need a generic there so we can capture the actual argument being passed in and use that to infer the return type. Let's capture this in a generic. We can say TName extends this. There we go. Bit of funky syntax there came out for a second.

3:01 Because this is a generic on the getFruit function itself, we should be able to capture banana in that slot. Great. Except now we're not getting the specific banana fruit back, we're getting any fruit back. That's not good either. We need to figure out the return type of this function.

3:21 We also need to remove undefined from that union, because we know this is never going to be undefined. We're always going to get back something from this array because we've specified the array, made it readonly, and inferred the thing from it.

3:36 Let's do that then. We have this getFruit. Let's do it on here first. We're going to use extract here. Extract is going to extract out TFruits number. We're going to extract out name TName. Now what this does is it's basically turning TFruits into a union, turning it into a union of its members, and then matching that to name TName.

4:10 What this means is we actually end up with the correct stuff being inferred here. We get banana, name banana, price 2, apple, name apple, price 1. Yes, beautiful. Except it's not really working inside our function because type undefined is not assignable to extract fruits number name. Fruits.find, we noticed this before, actually does return a undefined in its union.

4:36 We could try adding a non- assertion here to the end here. Now, of course, Fruits, all it's being inferred as is fruits. It doesn't quite get that we're able to extract out the proper thing by the name being passed in. I would say the best thing to do here is actually use an as annotation. We would probably need to use this as annotation anyway.

4:59 What we could do, even if we specified it in the return type as well, we would need to specify as the as inside the function there, too. I like to just drop the return type when we have this duplication in this case. Now everything is working. We've got our banana. We've got our apple. All the tests are passing. All the thing we're getting back is readonly.

5:22 If we add our orange back, let's say name orange and price 3, then we can get proper inference here. If we say this and orange, then that's working beautifully.

5:33 This is a really nice demonstration of how you can really get lovely inference when you pass in these complex config objects. You can make these beautiful little wrappers around functions so that you can...This is mostly type level programming here. The actual code that's going to be shipped to the browser or the bundle is really small, but we do get just this lovely layer of inference on top.