TypeScript Generic Rest Parameters and Tuple Types in Practice
By: Tim Coppieters
Tags: Web Development, Nodejs, Typescript, Programming, JavaScript

Authorization

In this article we’ll focus on how we handle step 4, the authorization, and how generic rest parameter types provide full type support for it. The framework facilitates our developers in the process by providing an authorize method, which takes a higher-order auth function and some arguments for it. The auth function is called with those arguments. If it returns false a specific UnauthorizedError will be thrown. This is then caught by the framework and a 403 response is returned to the client. The method itself is straightforward:

If we look more closely at the auth function, it is called with:

The instantiation of the controller (which allows the function to perform db operations, logging, etc. in context of the request) The other arguments given to the authorize function. These arguments are gathered using the rest operator and then applied to the auth function call using the spread syntax. Basically we are passing all the parameters of the authorize function to the auth function, except for the first one.

An example auth function that checks whether the job belongs to given user could be defined as:

Types

Now, we could type the authorize function saying that both rest arguments are of type unknown[]:

But then we are missing out on a lot of type checks when calling authorize:

The number of arguments required, because array types don’t have a fixed length. The type of arguments required, because the elements are homogeneously typed as unknown (meaning, all of them have the same type, unknown).

In other words there would be no proper type checking on the rest arguments, so the following calls are allowed:

await this.authorize(isJobOf, job)

await this.authorize(isJobOf, job, "id")

await this.authorize(isJobOf)

What we actually want to tell the type system is that the arguments that are gathered in the authorize’s rest args are exactly the same as those of the auth fucntion. We can try this by using generic types, saying that the authorize function’s rest parameters is of type P and the auth functions’s rest parameter is of type T. Then, we instantiate the auth function’s generic type with P so that T = P:

The type system will complain that args needs to be of type array, because that’s the only allowed type when using a generic type on rest arguments. This is solved by telling it that the generic types P and T both extend the Array type unknown[] (you could also use any).

Although it feels natural to type the rest arguments as an array, it is actually a special construct where the type system will use tuple types instead of homogeneous array types. Tuple types solve exactly the problem we had, namely they type an array with a fixed length and a specific type for each element.

When calling authorize with isJobOf, the type system will:

Match the signature type of isJobOf with AuthFunction<T>, setting T to the tuple [number, number] to match the types of the 2nd and 3rd argument of isJobOf.

As a result the generic type P of authorize is also set to [number, number].

So now the type system enforces the fact that the rest parameter of authorize need to be of type [number, number] and consequently that the 2nd and 3rd parameter need to be of type number.

So now the type system does exactly what we wanted it to do. Furthermore, amazingly enough when using VSCode it will even copy the right argument names of the auth function to show in the type signature of authorize when calling it (job: number, customer: number):

This seemingly simple result is all made possible by the intrinsic combination of rest arguments, the spread syntax and tuple types with generic rest arguments. How cool is that! We are so happy with the amazing work by the TypeScript team to finally come up with a type system that works with us and not against us!