Extracting Object Properties with Reduce and Generics
Here's the starting point of the pick
function:
const pick = (obj: {}, picked: string[]) => { return picked.reduce((acc, key) => { acc[key] = obj[key] return acc }, {})}
Since we need to capture the type of the object, we'll start by adding a TObj
type argument a
Transcript
0:01 We know that we're definitely going to need to capture the type of the object. That is for sure. We've got TObj here. Let's say object is TObj there. Now what we've got inside pick is we're capturing a number, b number, c number, which is perfectly reasonable.
0:19 Then we've got this picked function, which we need to be the type of those keys. We should be able to pass in a, b, but not d. That means we need to put in key of TObj there.
0:36 Oh, key of TObj. This is a classic one. This error is so annoying because it looks like we're saying, "I want an array of keyof TObj." In fact, what you end up with is it calls keyof on an array of TObj. What? That's really confusing. You end up with "Argument of type string array is not assignable to this."
1:03 What it actually wants you to pass in is things like concat and copyWithin and entries, all that stuff. Horrible, horrible syntactical error. The way to get around it is just to use the Array thing instead.
1:16 Now this will work. You'll get a, b, and c here. Isn't it so nice when the autocomplete just does exactly what you want it to do? Then d is going to be erroring for us properly there. Beautiful. That's pretty good.
1:30 Now, this result type, what are we getting back? We're still getting back an empty object. Oh no. reduce here, we've run into this error before, where inside this reduce, it picks up its type arguments from the thing that you pass in here. We've got this horrible little bit.
1:52 One thing we could do is we could say actually, we need to decide what type we're returning here because the object that we're returning is going to be different than the object we passed in. Currently, the only thing that we've inferred is the object that we're passing in.
2:10 We could return TObj, but then we're probably going to need to use the Pick utility type here. We return Pick TObj. Then what do we put inside here? We can't say, "typeof picked" or something like that, because that just extracts out the keyof TObj thing that we have there.
2:29 We actually need a second generic here, which is the things that we're going to pick from the object. Let's stick that here. Let's say, "TPicked extends Array," or, in fact, it's going to be...There's actually a really interesting thing here. We could say, "Array keyof TObj" here. We could put in TPicked.
2:56 Now we say, "Pick TObj typeof picked." I'm going to ignore this error for now. In fact, I'll just remove this for a bit because I actually want to investigate what gets picked up when you use an array inside here. My spidey senses are tingling. I'm thinking that might not be the best thing to do.
3:18 What we get here is we get a number, b number, c number in the first type argument, so in TObj, but then in the second type argument, we get an array of a or b. This is fine, but we've actually got to do a bit of extraction there to get out the thing that we want.
3:34 We don't actually care that there's an array wrapping them. All we want to extract is a or b. That's ideally what we would like in that slot. What we want to do is we want to say, "Pick TObj," and then say "TPicked." Currently, we have to do TPicked number. Isn't that so weird?
3:55 Now the result is actually typed as we want it to be. It's picking it properly, but we're actually having to extract out this array when it would be great if it just inferred it. We're having to do this extra little layer.
4:07 What we can do is we can actually say let's remove this array from the outside. We're saying, "TPicked" here. Now we're going to put the array here. Now, TPicked number, I don't need that number anymore because TPicked is no longer an array. It's actually going to be just a or b. Isn't that sweet? Super-nice.
4:30 Now then we've got an issue with the reduce. Let me just check that it's working. result now, result should...Whoops. What have I done there? result.a, and we've got b. If I remove b from the equation, then b disappears. We're just left with a.
4:47 If I add d, which is a boolean -- sorry, 1 -- then we can pick here. We can pick d. Then we get d boolean. Brilliant stuff. That's fabulous. Now our tests are still passing.
5:04 How do we make this reduce happy? Why is it moaning? It's moaning because type empty object is not assignable to type Pick TObj TPicked. The reason is that reduce is inferring this empty object. What we want to do is make it, basically, infer this inside its accumulator. This accumulator is still an empty object.
5:29 We can do that by doing this. Well, no. We can't do that by doing this because it's actually not caring about the accumulator. It's just caring about this still. I think what we need to do is do an as here.
5:47 This as, what it's doing is it's basically saying this empty object is going to be the Pick TObj TPicked. In the accumulator, we're basically saying accumulator key, which is keyof TPicked or keyof the TObj, is the object T key. We can't say, "acc.whatever = cool," because whatever is not assignable to, basically, Pick TObj TPicked.
6:19 This, I think, is the best way around it. We don't actually need this return type anymore because pick.reduce now has Pick TObj TPicked in its type argument. We can actually remove this if we want to. Goodbye. Done. picked Array TPicked. This is still looking good.
6:39 The result of pick is now still exactly the same, pick, blah, blah, blah, blah, blah. That's super-duper nice. What we end up with is exactly what we need. We've got our result, which is just using the pick utility type.
6:52 It's quite nice that it shows you this. It would be nice if it just showed you the resolved output, but this is fine, I think. We end up with the test are passing. We've got the as working too. Nice.
7:05 The lesson here, I think, is to make sure you're using two generics. Make sure that the thing in your type argument is the lowest possible level. This TPicked here should be just a or b instead of an array of a or b which you've then got to extract out.
7:22 That just helps the compiler a little bit. It makes sure that you're not doing extra work for the compiler. Also, the readability of these type arguments is really, really nice. We do need to extract the entire object here because we care about the type that it results in.
7:38 For instance, we can pass strings into here. Then a would be typed as a string. result.a is going to be a string. That's why we need to capture the entire object there instead of just capturing the keys like we've seen before. There we go. Nice challenge. Well done.