The Art of Type Arguments 9 exercises
solution

Constrain a Type Argument to a Function

Solution 1

For the first solution, the type arguments contains TParams and TReturn.

With TParams extending an any array, any amount of arguments can correspond to it, and TReturn will be the return:


const makeSafe =
<TParams extends any[], TReturn>(func: (...args: TPa

Loading solution

Transcript

0:00 This is the first of, of course, two solutions. We have our makeSafe() function where we have two things inside the type arguments, TParams and TReturn. The function that we're representing here, we can put in any amount of arguments which corresponds to TParams. Then, we can return anything here. We put inside the result in TReturn. The args are TParams here.

0:27 Let's see this working. We can see that makeSafe here, the two slots, we have an empty tuple, which represents no parameters, and then number here. We have makeSafe empty, arguments, and number. This means that when we call it, we can't pass anything here because the arguments this function takes are in fact nothing. We can't put anything in there.

0:52 Whereas down here, we have our makeSafe. It has a: number and b: string. This is what's called a named tuple. It's quite an unusual part of TypeScript. It's basically capturing the name of the argument that we give the function here. You can see that I changed it to a c and now it's represented as a c here.

1:13 What we get is this function. We have a: number and c: string here, really, really cool. This means that we get errors when we call it with the wrong stuff. That's what we're doing. We're saving those two things in our generic slots. Our TReturn is unconstrained because this could return anything. It could return void if we wanted it to.

1:37 On makeSafe, let's just say it just returns this. Now, func is going to be type: "success"; result: void as well. It can be literally anything that they return. It can be a promise. It can be whatever we want to, although maybe a promise is a little bit tricky. That might be an interesting extension, some way to add on to this exercise if you wanted to restrict promises.

2:02 Why did I choose then extends any array for TParams? If I don't have this, then TParams is going to yell at me because a rest parameter must be of an array type. I can choose extends unknown array here. I think this will actually work. This behaves exactly the same as the other thing.

2:22 If I pass in this, then it's a: number, b: string. Any array or unknown array is the same here, but it's basically representing any array that you could pass in. Of course, we could represent this as Array<any>, like this.

2:38 The second solution then is I've done this in a slightly different way. Here, we're saying TFunc extends (...args:any [ ) and then any here. Instead of representing it in two generic slots, we're representing it in just one.

2:58 It's very similar, apart from we're extracting it in a slightly different way so func is now TFunc. The args are Parameters<TFunc>. The result is ReturnType<TFunc>.

3:08 If we look and compare it to the previous one, it's where you do the extraction. You either do the extraction inside here and here, or you do the extraction further down where you need it.

3:22 If there were another one of these union types, type: "whatever", and then everything is, I don't know, returnType: ReturnType<TFunc>, parameters: Parameters<TFunc>, then it would start to get a bit silly. We would start to look at maybe we want to represent it like this instead. If there's only one call site, it seems fine to do it like this.

3:48 If we look at the call site, then you can see we get this to number here, func, blah, blah, blah, blah. If we look here, we see a: number, b: string. Everything is being represented in the generic slot.

4:02 This is entirely up to you. Again, this is the same argument as we had in the previous couple of exercises. I like seeing all of the pieces here. Whereas if we look at the call sites over here, it's a little bit less obvious what this represents. They take up the same amount of space.

4:25 This is kind of dealer's choice as to which one you choose. I did want to lay out both of the options. The main lessons to take from this is to make sure that when you are representing the parameters of a function is to use an array type. Also, understand that spreading these back in makes sense and is the only way to constrain your function.