Using Overloads and Generics to Type Function Composition
The way to solve this problem is to think about it through the function overload lens.
Function Overloads are Necessary
The reason this needs function overloads is that we need to capture different numbers of generics based on what the user passes in.
0:01The way to solve this problem is to think about it through the function overload lens. The reason this one needs function overloads is that we need to capture different numbers of generics, based on what the user passes in. This is tricky. Let me show you why it's necessary. Then we'll talk about possible alternatives. I'm not sure there are any.
0:30 Because I'm going to use function overloads for this, I'm going to say, "export function compose" and change this into a classic function body. Then I'm going to return this. Now we've got our compose function working.
0:43 For now, I'm just going to keep this super-loose, like an implementation signature, because I'm going to do all of my typing, actually, in the top level. I'm going to probably keep these anys around as well.
0:57 We are going to export a function compose. compose is going to take in just one argument. That argument is going to be a func, where it's going to take in...Let's say I'm going to start naming these generics like T1 and then return T2 here.
1:16 I'm going to stick this inside here. You'll see why I'm doing this in a minute. Then compose, which lacks return-type annotation. Now we get back a function, which takes in t1 is T1 and returns T2.
1:35 What have I done here? You'll see why I'm naming these like this in a minute. You can think of T1 as the first input that gets taken in. We take in a function that takes in T1 and returns T2. Then we just basically return that function. You can see that these two are exactly the same signature.
1:56 I can try this now. I can say, "compose." Let's say I just use a String constructor. Now what we get back is...We pass in any. We return string. That any looks like it isn't being captured. Let me actually do something different there where we return a string. Sorry. Blah, blah, blah, blah, blah.
2:13 Then we take in a, which is going to be a number, for instance. We take in a. We just ignore it. We return a string, whatever happens. Now we get compose number string. T1 is being captured, and T2 is being captured.
2:27 This is good, but we're not handling the case where we have more than one generic in or more than one function being passed in. Let's do that. I'm going to copy and paste. I'm now going to add T3 onto here. We've got func1. Then I'm going to say, "func2."
2:47 Func2 is going to take in T2 as an input. It's going to say t2 is T2. It's going to return T3. Now the function that we end up with, it takes in the input of the first parameter and actually takes in the output of the third parameter.
3:06 You see that? Whoo. Blimey. Now, if we compose -- let's say we have a function, which is here, returning abc, takes in num number -- you can see there that we have number string. Now, if we add a second one here...We say, "t2." We get t2 back here. Then we return t2.parseInt, for instance. Whoops. No, no.
3:39 ParseInt t2, which of course is going to fail, but it shows them that what we end up with is number, which is the number here, then string, which is what's returned from there, and then number, which is the third parameter. Blimey. You just end up with a function that takes in a number and returns a number.
4:01 You can probably see where this is going. Where we have compose, let's say, "T4." Let's say func3 is now...We've got t3 T3. This returns T4. That one is T4. Let's go all the way up to five before you start to lose your mind. T5. This one is T4 functions here. That one is T4 to T5. This one is T...Inside here. Blimey.
4:35 Now what you can see is we're now getting this passing. You've now got addTwoAndStringify, which takes in addOne, addOne, and String. The way that TypeScript resolves its type arguments is working in our favor here.
4:52 If we remove this error, you can see that String basically says -- so good -- "Argument of type StringConstructor is not assignable to parameter of type t1 any number." This one is now expecting a number back. This string is incompatible.
5:10 If we move these around and say addOne first and then String, then it's absolutely fine. String can accept anything into it. You end up with number, number, string. t1 is a number. t3 is a string, which is what's returned.
5:24 This then is pretty much how you solve this problem. This problem is out there in the wild. It's been solved in this way by libraries like io-ts. There's another one that I can't quite remember off the top of my head.
5:38 Oh, yeah. It was solved this way by Reselect as well, which Mark Erikson worked on. This is a pretty well-respected solution. Well done, if you managed to find it. This one is super-hardcore.