How to make TypeScript infer the number and types of parameters in a function

Hugo Martinet
lalilo
3 min readMay 2, 2022

--

We will see how to type a function to make sure its first argument determines whether there should be a second one, along with its type. This article presents an actual typing problem we encountered at Lalilo and how we solved it.

In our frontend apps, we made our routing typesafe by generating URLs with a generateUrl function instead of writing hard-coded raw strings. When the URL template contains a route param (with a : identifier), we need to specify as second argument an object containing the route params values. Forcing the caller to specify this second argument is crucial, otherwise the user would experience a routing error in our app.

Prerequisites

If you are not familiar with the following TypeScript concepts, we recommend you read the docs to better understand the article.

With Rest Parameters ✅

As a prerequisite, we will define a UrlParams<U> type which extracts the param keys from an URL-like string.

Note: Understanding this type is not in the scope of this article. We are introducing it here for the reader to see how it can be used (lines 7, 8 and 9).

1. First, we should use generic typing to manipulate the type of the first parameter. This will allow to make the first parameter’s type infer the second’s.

2. Then, we will spread the rest parameters, typing them with a custom type which depends on the type of the first parameter.

Notes:
- Rest parameters’ type should always be an array of types.
- For now, the function can have as many rest params as the caller wants, and we do not know their types.

3. Finally, we will make sure that:
- if the URL does not have params, the rest parameters are empty ;
- if the URL has params, there is one rest parameter which is an object.

4. Additional step: It is interesting to go further in the typing of RestParams<U> to make sure that the second parameter is not only an object, but the keys also correspond to the actual route params.

Notes: More details on Record type here.

With Optional Parameters ❌

Originally, our first attempt was to make the second parameter optional. However, this does not solve the problem, because we want the second parameter to be required in some cases.
e.g. In generateUrl, the second parameter should NOT be optional when the first argument has URL params, because the function needs values to set for the params.

Conclusion: Optional parameters allow the developer to decide whether or not they want to pass a second argument, it can never force the developer to do so.

With Function Overload ❌

Our second attempt was to write overload signatures for the function, as it allows us to give the function several signatures (more documentation here). e.g. The goal is to have 2 signatures for the function: one with an no-params URL as first parameter and no second parameter, one with a URL with params as first parameter and the params values as second parameter.

Unfortunately, we could not find a way to define the UrlWithoutParams type, which resulted in the impossibility to use function overloading here.

Conclusion: While function overloading allows the developer to give several signatures to a function, it is not usable if the developer is unable to define for each case the precise type of each argument.

Conclusion

There are many TypeScript features we can use when typing a function. Among them optional parameters and function overloads are extremely useful to make a function more flexible while still typesafe.

In some more complex cases though, using the rest parameters is a great option to have the first parameter infer the number and types of its other parameters.

This is probably not the only way to solve the problem we faced at Lalilo. You can find here a TypeScript playground with the solution and its usage. Feel free to ask questions or propose other solutions!

--

--