Use function overloads and generics to type a compose function

Function overloads in TypeScript are an incredibly powerful tool for building. things that really are not possible to build otherwise. Here we have a compose function. which takes in multiple other functions and produces another function.

This is a key tenet of functional programming.


function compose(...args, any[]) {
return {} as any;
};

So here if we just had an addOne function we would just have a number which returned another number.


function addOneToString = compose(addOne);

But what if we wanted to chain several functions together? With the result of each previous function piping into the next.


const addOneToString = compose(addOne, numToString, stringToNum)

How would we achieve this? We use function overloads.

We've got the actual implementation of the function here and you can see that it's kind of stubbed. It's just got an any inside it, which TypeScript allows you to do.


function compose(...args, any[]) {
return {} as any;
};

What's actually interesting are the overloads. So we have a function that takes in a function and returns a function.


export function compose<Input, FirstArg>(
func: (input: Input) => FirstArg
): (input: Input) => FirstArg

We have the first signature or the first overload for compose, which takes in two generics. It has the Input which is the very first thing you input into the returned function and then you have the first argument.

So here we're saying FirstArg, because further down we're going to add a SecondArg and a ThirdArg.

This is only triggered if you use one argument.


function addOneToString = compose(addOne);

If you add in a second function there then the second overload is triggered. So now you have an <Input, FirstArg, SecondArg>. And here your arguments func and func2. func returns the FirstArg, and that is the input for func2, which returns the SecondArg.


export function compose<Input, FirstArg, SecondArg>(
func: (input: Input) => FirstArg,
func2: (input: FirstArg) => SecondArg
): (input: Input) => SecondArg

And the third overload here it means it just does the same thing again but adds a third arg to it.


export function compose<Input, FirstArg, SecondArg, ThirdArg>(
func: (input: Input) => FirstArg,
func2: (input: FirstArg) => SecondArg,
func3: (input: SecondArg) => ThirdArg
): (input: Input) => ThirdArg

What this does is protect you from cases where the output from one of the previous functions doesn't match the input of the next function.

So for example you can't run numToString and then addOne because numToString returns a string and addOne takes in a number.


const addOneToString = compose(numToString, addOne)

And this means that function overloads are incredibly powerful for these sorts of compositions.

Transcript

0:00 Hello, folks. Function overloads in TypeScript are an incredibly powerful tool for building things that are not possible to build otherwise. Here, we have a compose function, which takes in multiple other functions and produces another function, which is a key tenet of functional programming.

0:20 Here, if we just had an addOne function, we would have a number which returned another number. Then, if we wanted to take a num to a string, so we add one, and then turn it into a string, then finally, it goes number to string. Then if we want to turn it into a number again, we go stringToNum, and finally it's number to number.

0:40 How do we achieve this? Well, we've used function overloads. We've got the actual implementation of the function here. You can see that it's stubbed. It's just got any inside, and TypeScript allows you to do this. Currently, we're returning this as any so I don't need to look at that too hard.

0:56 Instead, look up here. We have the first signature of the first overload for compose, which takes in two generics. It has the Input, which is the very first thing you input into the returned function. Then you have the first argument.

1:10 Here, we're saying FirstArg because, down here, we're going to add a SecondArg and a ThirdArg. What it returns is a function which essentially just returns the function that it's passed there. Now, that's only triggered if you use one argument here, so only if you do this, if you add one.

1:28 If you add in a second function there, then the second overload is triggered. Now, you have an input, FirstArg, and a SecondArg. Here, you have func and func2. This returns the FirstArg. Then the FirstArg gets passed in to the second function there, which returns the SecondArg, which returns a function which takes in the input and returns the SecondArg.

1:48 The third overload here means it just does the same thing again, but adds a third arg to it. What this does is it means that you can have things like where you add one, but imagine if you go numToString first, and then you try to add one to it. This means that there's an error here because the string is not compatible with a number signature.

2:11 This means that function overloads are incredibly powerful for these sorts of compositions.

Function overloads can be used in conjunction with generics to make incredibly complex and dynamic type signatures.

Here, we make a compose function - incredibly useful for functional programming.

Discuss on Twitter

More Tips

Assign local variables to default generic slots to dry up your code and improve performance