Creating typings for curry using TS 3.x
Compose all the things 🎉
Functional Programming is all about composing functions together and that means we need to make the output of a function to serve as the input of the next one. In order to do so, it’s convinient to make functions have a similar “shape”, thats why in lambda calculus all functions are 1-ary, meaning they all receive one argument and return one value.
Thats when curry-ing comes in handy as it lets you define your function with all the parameters at once and then call them however you like.
If you just want to use curried functions in TypeScript and have the type checker to correctly know what’s what, I recommend you install ramda and @types/ramda with npm. This post is ment to learn how to create typings for a generic curry function and in the process learn some of the advanced features of TypeScript such as conditional types, richer tuple types and function overloads.
If you want to see the full example, here is a link to the repo.
To create the typings for a generic curry function we’ll start by creating a simple concrete curried function and work our way up.
So we are going to try to curry the add function, wich takes two numbers and adds them together. In order to do this, we’ll use function overloads which is a mechanism to provide different call signatures for the same function.
The following type declarations says that if we call add with two numbers it will return us the addition, but if we only pass one argument, it will return us a new function, that receives the second number and returns the addition.
Notice that we have three lines with the add function, the first two are the function overloads, which specifies the different ways the function can be called, and ends with a “;”. The third add is the implementation of all the overloaded functions, and as such, the type must be generic enough to contemplate all the cases.
The last line is not actually needed to create the add function but it’s there to help us understand a different way to describe overloaded functions. If you hover over OverloadedAdd in the playground you’ll see that we can describe a curried function of two arguments like this:
With a little refactoring we can extract Curry1 as the base case, an 1-ary function.
And from here we can define to the n-th that we want
This means that if we have a curried function named bar that accepts three arguments we can type it using Curry3. This will make bar to be overloaded with 3 different call signatures (with one, two or three parameters). If we call bar(1) we’ll get a Curry2 function, because there are still two ways we can pass the remaining arguments.
Unknown number of arguments
So far we can type curried functions of n-th arguments (we only defined 4-th, but you get the idea), but the curry function must work with functions of any lenght, so we must have a way to indicate TypeScript which CurryN type to use, we need conditional typings.
Conditional typings is a feature added in version 2.8 of TypeScript, and they allow us to set a type depending on a condition. It works like the ternary operator but with types. In the following example we can notice that the type of x will depend on whether TypeX matchs or not with SomeShape.
So in our case we’ll define a conditional type called VariadicCurry which will help us determine which CurryN type to use. The name comes from Variadic Functions which are functions that can receive a variable number of arguments. It works by nesting the type condition, so if T is a tuple of four numbers it will return a Curry4 type, if its a tuple of three, Curry3, and so on.
If the type T doesn’t match with any of the tuples, we return the new unknown type introduced in TypeScript 3, wich is similar to any, but safer in the way that you can’t assign it to every variable, you need to cast it, and ideally after checking it’s type dynamically.
Now we have all the tools to define our curry function 🎉.
There is a lot going so lets break it in pieces. The function curry accepts only one argument named fn, which is the function to be curried. The return of curry will be of type CurryN depending on wheter T is a 1-tuple, 2-tuple, etc. We know T will be an n-tuple because we defined it inside the angle brackets <T extends number>. And we define that fn will be a function of n-th arguments (defined by …args: T) that returns a number.
The last part is possible thanks to another set of features added in version 3, which allows you to interact with function parameter lists as tuple types.
With all this in place we have defined the types for our curry function… that only works with numbers. ¯\_(ツ)_/¯️
Numbers are great, but we made it this far, lets try to make the function work with all types. We’ll start by making the the return type generic on R. We’ll refactor our code making fn return a type we know nothing about called R. This type propagates all the way trough our other definitions, making each CurryN generic on the return type.
Making all CurryN generic in their arguments its fairly easy as well, we just have to make our types to accept extra parameters.
And to make VariadicCurry and curry to use the new parameters we need to do one more “trick”.
The VariadicCurry type will now match T to a n-tuple ignoring the argument type (because we use any instead of number) and only caring about the arity of the tuple. When we return a specific CurryN we use the indexed types T, T, T and T to specify the correct arguments types.
And that’s it! We now have the typings for a curry function that works with an unknown number of arguments and any type you may think. If you want to try the full code you can browse this repo or use this playground.
If you like this post send love ❤️, all feedback is welcomed. You can follow me on twitter as @sherman3ero for more information on TypeScript and functional programming.