Challenges 5 exercises
solution

Narrowing with Arrays and Generics

Let's start by capturing what we get inside the wrapFruit in a generic.

We'll add <TFruits> to wrapFruit , and then create an array called fruits that's going to be a <TFruits> array:

Loading solution

Transcript

0:00 Let's do this. We know that we're going to need to capture the thing that we get inside wrapFruit in some sort of generic. Let's do that first. We're going to say, "TFruit." I'm not going to put a constraint on it yet. Let's just say fruits is going to be TFruit array.

0:17 Now what we've got here is we've got wrapFruit. This has been captured in name string, price number. We don't quite have enough information here because fruits currently could be anything. TFruit could be a string, could be a number, could be unknown, whatever. It doesn't have a name property on it.

0:37 We need to say, "TFruit extends Fruit." Now we know that Fruit is going to be...It's going to have a name property. It's going to have a price property as well. That's going pretty well.

0:49 Now we need to figure out this getFruit thing. getFruit, it's going to rely on the exact inferred literal value of the name property of all of these, like of TFruit here. This gets tricky because we're not currently inferring the deep inferred literals here. We're just grabbing -- what is it -- name string and price number.

1:16 Let's do that. We can do that with F.Narrow. If we say, "import from ts-tool belt" -- let's grab the F -- now we can say...On here, we're going to say, "F.Narrow TFruit." This is good because what we get here is we get a union of the members of the array, which is inferred to their literals.

1:39 That's really, really useful because what we can do now is we can then say this name is going to be TFruit name. Very, very cool. This TFruit name, TFruit is now basically a discriminated union that always has a name property on it. What that means is we can index into it and just grab out the name property from that union.

2:03 If I copy this out and if I say, "type Whatever =" this...I can say type, let's say, Discriminator is Whatever. Then I index into it and get the name. By doing that, I end up with apple or banana here. If I change one of these to orange, then I get orange or banana instead. That's really good. I now get autocomplete based on the things that I pass in to the fruits.

2:31 There's a weird issue with this though, which is with fruit.name here, you can see that fruit is like, "Oh my God. What is this type signature? Something really crazy is going on here." What we're looking at here is actually the internals of F.Narrow. F.Narrow is being blown out, beyond its boundaries. That's because we're using it slightly wrong.

2:56 What we should be doing here is F.Narrow should actually be applied at the highest possible level. You shouldn't be doing F.Narrow TFruit inside an array and then putting that inside an array. I think what we're going to need to do instead is say TFruits extends an array of Fruit. Then the fruits here is going to be F.Narrow TFruits.

3:20 Now what happens is we've got an error here, but fruit is now inferred properly. This is going to be name string, price number. This is a good tip with F.Narrow, is to always apply it at the top level. Don't be doing like -- I don't know -- obj, whatever, something, and then F.Narrow inside here. It might not work very well, or at least it doesn't work very well when it comes to arrays.

3:45 Now, instead of capturing the individual members of the Fruit inside the type argument, we're now capturing the entire thing. You see that. It's now a tuple with all of the members inside it. This discriminated union trick no longer works.

4:02 What we need to do first is extract out the members here. We can do that with number. TFruit is not going to be TFruit, but TFruits, so TFruits number name. Now e get our beautiful inference back, where we can say, "apple, banana." That's working great, except that the thing that we're getting back isn't very happy. It's still typed as unknown.

4:28 What we're going to need to do is, because we need to grab the specific thing that comes back, we're going to need to actually make this a generic function again. getFruit itself is going to be generic because we need to capture the thing that's being passed in here. Let's say we've got TName. That's going to extend this constraint. Let's do that now. Then name is TName.

4:55 Now, when we go down here, then getFruit is being captured inside that. That's great. apple is being captured. banana is being captured. Fabulous.

5:04 Now we need to figure out this return type. The return type is going to be...We're basically going to need to go inside TFruits number and extract out a member from that union that matches an object with the name that we've just inferred. The way to do that is with an extract. We can Extract TFruits number and then name TName.

5:32 What? Let's actually just break this down a little bit. We can say type Fruits is going to be...I'm going to just grab the thing that's being inferred from here. This is a nice little way to do it. We've got our fruits then. Now what we can do is we can say, "type Result =." We're going to Extract. Actually, we'll just call it, "Fruits number." This ends up as a union type of all of the members.

6:07 Now what we can do is we can Extract from there name and, let's say, apple. Now what we end up with is apple, price 1. If we change this to banana, then we end up with banana, price 2. Beautiful. That's the logic that we're going to be using inside our generic function.

6:28 Great tip, by the way. If you're ever deep in the rabbit hole with a generic type and you need to just pull it out, just see what's being inferred, pull it out into its own types, and mess about with it.

6:38 This is now working beautifully because banana is being inferred as name banana, price 2, and name apple, price one. All our tests are passing, except that there's an error inside here.

6:51 What's the error? Type Fruit or undefined is not assignable to Extract TFruits number name, name, name. We're doing something really, really clever with TypeScript, which is we're saying we know that the thing that's being passed in, we're going to be able to get it by name.

7:09 We're then returning something that's never going to be undefined. It's always going to be the thing that we're passing in. That's going to be really nice for our users to use, but TypeScript doesn't really get it.

7:22 We could say, "as" this. In fact, that works really nicely. There we go. What I like to do in these situations where I'm doing an as which is exactly the same as the return type, I just like to delete the return type. This means that you only need to have one single source of truth here. It means that TypeScript infers this properly, and our inference is still really good.

7:44 We're using an as here because we know a little bit better than TypeScript what's going on. We're forcing it to basically say this is the thing that we understand it to be. We know better than you, TS. Hopefully, that makes sense as an explanation.

8:00 We have an outer generic that we use to represent the entire array of Fruit. We use F.Narrow on that to grab out the values. We use a generic on getFruit itself, based on the name. We use the name in the argument itself.

8:16 Then we do a find. Then we basically do the find on the Extract here as well. We're replicating the runtime inside the type level to Extract out TFruits number and pull out the name there.