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

Play Type Predicates

Type Predicates

1 min

Play TypeScript 5.1 Beta is OUT!

TypeScript 5.1 Beta is OUT!

2 mins

Play How to Name your Types

How to Name your Types

4 mins

Play Don't use return types, unless...

Don't use return types, unless...

4 mins

Play TypeScript 5.0 Beta Deep Dive

TypeScript 5.0 Beta Deep Dive

6 mins

Play Conform a Derived Type Without Losing Its Literal Values

Conform a Derived Type Without Losing Its Literal Values

1 min

Play Avoid unexpected behavior of React’s useState

Avoid unexpected behavior of React’s useState

1 min

Play Understand assignability in TypeScript

Understand assignability in TypeScript

2 mins

Play Compare function overloads and generics

Compare function overloads and generics

1 min

Play Use infer in combination with string literals to manipulate keys of objects

Use infer in combination with string literals to manipulate keys of objects

1 min

Play Access deeper parts of objects and arrays

Access deeper parts of objects and arrays

1 min

Play Ensure that all call sites must be given value

Ensure that all call sites must be given value

1 min

Play Understand how TypeScript infers literal types

Understand how TypeScript infers literal types

1 min

Play Get a TypeScript package ready for release to NPM in under 2 minutes

Get a TypeScript package ready for release to NPM in under 2 minutes

1 min

Play Use assertion functions inside classes

Use assertion functions inside classes

1 min

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

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

2 mins

Play Know when to use generics

Know when to use generics

2 mins

Play Map over a union type

Map over a union type

1 min

Play Make accessing objects safer by enabling 'noUncheckedIndexedAccess' in tsconfig

Make accessing objects safer by enabling 'noUncheckedIndexedAccess' in tsconfig

1 min

Play Use generics to dynamically specify the number, and type, of arguments to functions

Use generics to dynamically specify the number, and type, of arguments to functions

1 min

Play Use 'declare global' to allow types to cross module boundaries

Use 'declare global' to allow types to cross module boundaries

2 mins

Play Turn a module into a type

Turn a module into a type

2 mins

Play Create autocomplete helper which allows for arbitrary values

Create autocomplete helper which allows for arbitrary values

2 mins

Play Use deep partials to help with mocking an entity

Use deep partials to help with mocking an entity

1 min

Play Throw detailed error messages for type checks

Throw detailed error messages for type checks

1 min

Play Create a 'key remover' function which can process any generic object

Create a 'key remover' function which can process any generic object

1 min

Play Use generics in React to make dynamic and flexible components

Use generics in React to make dynamic and flexible components

1 min

Play Create your own 'objectKeys' function using generics and the 'keyof' operator

Create your own 'objectKeys' function using generics and the 'keyof' operator

1 min

Play Write your own 'PropsFrom' helper to extract props from any React component

Write your own 'PropsFrom' helper to extract props from any React component

1 min

Play Use 'extends' keyword to narrow the value of a generic

Use 'extends' keyword to narrow the value of a generic

1 min

Play Decode URL search params at the type level with ts-toolbelt

Decode URL search params at the type level with ts-toolbelt

2 mins

Play Use 'in' operator to transform a union to another union

Use 'in' operator to transform a union to another union

2 mins

Play Derive a union type from an object

Derive a union type from an object

2 mins