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.
More Tips
Type Predicates
1 min
TypeScript 5.1 Beta is OUT!
2 mins
How to Name your Types
4 mins
Don't use return types, unless...
4 mins
TypeScript 5.0 Beta Deep Dive
6 mins
Conform a Derived Type Without Losing Its Literal Values
1 min
Avoid unexpected behavior of React’s useState
1 min
Understand assignability in TypeScript
2 mins
Compare function overloads and generics
1 min
Use infer in combination with string literals to manipulate keys of objects
1 min
Access deeper parts of objects and arrays
1 min
Ensure that all call sites must be given value
1 min
Understand how TypeScript infers literal types
1 min
Get a TypeScript package ready for release to NPM in under 2 minutes
1 min
Use assertion functions inside classes
1 min
Assign local variables to default generic slots to dry up your code and improve performance
2 mins
Know when to use generics
2 mins
Map over a union type
1 min
Make accessing objects safer by enabling 'noUncheckedIndexedAccess' in tsconfig
1 min
Use generics to dynamically specify the number, and type, of arguments to functions
1 min
Use 'declare global' to allow types to cross module boundaries
2 mins
Turn a module into a type
2 mins
Create autocomplete helper which allows for arbitrary values
2 mins
Use deep partials to help with mocking an entity
1 min
Throw detailed error messages for type checks
1 min
Create a 'key remover' function which can process any generic object
1 min
Use generics in React to make dynamic and flexible components
1 min
Create your own 'objectKeys' function using generics and the 'keyof' operator
1 min
Write your own 'PropsFrom' helper to extract props from any React component
1 min
Use 'extends' keyword to narrow the value of a generic
1 min
Decode URL search params at the type level with ts-toolbelt
2 mins
Use 'in' operator to transform a union to another union
2 mins
Derive a union type from an object
2 mins