Making TypeScript Stick Intermediate Type Challenges: ReturnOf & Split
Transcript from the "Intermediate Type Challenges: ReturnOf & Split" Lesson
>> Now let's go to round two. Okay, so this type actually exists in TypeScript, it's called a return type. But I have had to rename it so we don't collide with that utility type. We're gonna try to make this ourselves. So just to look at this before I flip over the playground, here's a good example.
[00:00:21] I have a game of rock-paper-scissors that takes in a number and translate that to literally the string rock or paper or scissors. And I want my return type to be. Rock or paper or scissors I want these two types to be equal. So let's try it out. Does everyone understand what the ask is here?
[00:00:54] So for example, So I would hope that P would be a promise void right, cause promise resolve is a function. So return types first of all, it's assumed that functions are the things that have a return. Therefore, we're only dealing with function types of some sort. So I would hope that this when we're done turns into a promise, so that resolves to a void.
[00:01:46] So first how can I build in this constraint of making sure that we're only using function types here? Does anyone have any ideas? I'll give you two choices. You tell me which one you would prefer. We can either use a conditional type. Remember, we've done things like this, condition if true, if false, right, and that seems like we've done something that kind of looks like this as a way to check whether something's an array, or array ish.
[00:02:26] We've also done this something like that. So I'm actually going to lead us down this path. So I wanna see like is f, does it extend? And then I want like I want to type that represents any callable thing. What's any callable thing conceptually, what's, what are the things we need to be flexible around?
[00:02:53] Like, I'll give you this. That's an example of a callable zero argument function that returns a number. So what if we wanted to accept functions with any number of arguments? That's probably something we need to do, return type has to work for functions with lots of arguments or none.
[00:03:22] How would we make this generic enough?
>> Like, spread the arguments?
>> Yeah, spread but when what type of the arguing to be maximally flexible.
>> Yep an array of entities that's give me zero or give me a million, and they can all be whatever you want and then here's the return type.
[00:03:47] So our next bit of magic is gonna be dealing with this. This is in fact what we want, right? This is the thing we want. Has anyone ever captured something like this and sort of plucked it out? Anyone used the infer keyword before? All right, in a conditional type you can do this.
[00:04:13] RT for return type, notice I'm getting no error here yet. And what we're saying is yet there's something here there is something here that I want to almost as if is a type arm, grab whatever that is, get that and then I wanna use it and spit it out.
[00:04:39] And there we go all our test cases started to pass. And here's our P promise, sorry it's a promise unknown under promise void. So in summary, we're able to peel away the return type of function by saying first of all we said okay we need to make sure we match the right thing.
[00:05:01] And we made the most flexible function type we possibly could which would have been this right function that takes any number of anything and returns anything. It's like any callable thing will match that type. And then we replace this any with infer RT, which lets us define a new, almost like a type parameter.
[00:05:28] We can use it just like a type parameter over here actually look at the tooltip type parameter, right. So and then from that point forward in any of these branches of this conditional type we can use that. We could use that as flexibly as if it were over here all, right?
[00:05:46] So you could pass it through like an indexed access type, you can do whatever you want to do with it. So there we go, return up. And I wonder, If in here I can see the core thing. No I can't but I can go over to my editor real quick, let's see how this was implemented.
[00:06:20] Okay, so this is the equivalent in core and the TypeScript declaration files. So they have a constraint here, which will give people early feedback when they tried passing something that's not a function. And remember how we said like, we can, we don't have to have. This is when we're talking about variadic tuples.
[00:06:42] We're talking about like spreading an array is T versus whatever. So I guess we don't even need to do an array of anys cuz it's sort of spreading anything, and then here we go, T extends this, here's our infer R. And they're kind of, instead of falling out with a never, they chose to fall out with an any.
[00:07:08] This is more compatible this will but it'll fail in ways that may not be obvious to you like if I pass in actually it won't right. Cuz you'll get an error here for any non function cuz you won't match that constraint on the generic. And then over here like I don't really see any situation where you'd end up going down this path so it doesn't really matter what they put here.
[00:07:34] But cool we just made one of TypeScript's types core utility types. All right, Split, so I've been suggesting that we go here for a little while now. How do we do effectively the type version of string.split using only types? So in this case if I want to split by space, I should get up this list of broken words.
[00:08:16] How are we going to do that? I literally see jaws agape in the room. [LAUGH] Is it even possible? I'm gonna tell you straight up I don't quite I have this one memorized. So we're gonna work through it together and you'll see how I kinda approach this. So it is likely recursion will be necessary here.
[00:08:37] Because what we can do with a template literal type is, we can certainly figure out how to extract something to the left of. Or something to the right of some thing in the same sense that we were able to do ends with remember, like ice cream ends with cream.
[00:08:58] And we can actually use that infer keyword to pluck something out of a larger string, and to keep moving forward with that. So let's see if we can handle some basic cases first. So we can say, all right, we've got s for the string and set for the separator.
[00:09:18] So if s extends, string and separator sorry s extends and then we got to do infer rest. So that's like the remaining piece of the string or that's the piece of the string to the left of the separator. Okay, so let me experiment with what we have here.
[00:09:55] So split and then we'll say hello world. Split by space. And what is the type of X? It's just hello world that didn't seem to do much of anything. Let's try the other way. What's going on here? Are there, nope, just one space. Okay, let's see if s extends.
[00:10:29] The separator, what I probably need is, I need this. I can flick out two things. Yep, there's hello coming out. And then what happens when I use t here? There's world code coming out.
>> And his rest here is that like a keyword in TypeScript?
>> Nope, it's just another type parameter.
[00:11:03] So you can use more than one infer in a situation like this. So I'm getting kind of the left and the right part of the first occurrence of this separator, so just to be clear, see how I'm getting worldspace again. So it sort of it matches almost like a regular expression caching right it'll match on the first occurrence.
[00:11:29] And then we can get everything that happens before leading right up to the first occurrence and then the rest. So what I want is to kinda grab this first thing and I want to put it into a tuple. Like this, And so that'll give me the first thing.
[00:11:54] And then I want this. Recurse in, so what's our new string, it's T, it's the tail, T for tail, and the same separator. And what are we gonna get? Hello world again, we're getting real close, you had an answer for us. When we were doing our concatenating two arrays, do you remember how we got that concatenation to happen?
>> Spread or destruction?
>> What's going on here? A rest element type must be an array type. What if we do this? So close, There we go, okay, we have some of the things passing here. I'm not sure if I'm going to get through through all of them.
[00:12:51] But so let me talk you through what we have so far. We have effectively this is our condition, right? It's everything to the left of this question mark. And we're saying does s match this pattern of some word? Some texts that does not match the separator followed by the separator, followed by the any kind of text that may follow, might be a mix of separators and other things.
[00:13:18] If that's true, place the word to the left of the separator, the space in this case. That's the first thing in the tuple, and then do this whole process again. Passing the right side of the separator in as the new string so it's like we're breaking this down now.
[00:13:41] And then if nothing matches, if for example we just said we did this. I would expect just one tuple to come back just like string.split it'll just say, all right here you go. It's like the whole thing, right? Okay, so I think we're on the path let's see what tests are failing.
[00:14:06] Interesting, okay, so here's an edge case. We have an empty string. Splitting by an empty string seems to be causing us problems. Splitting an empty string by an empty string seems to be causing us problems, okay? Little edge case-y there. Splitting an empty string by Z seems fine.
[00:14:25] And that does not surprise me because that's this case right here. We're sort of falling straight through. And then splitting the set of all possible strings, an arbitrary string. Remember we're dealing with types here, like effectively when we don't have string literals. The string type just sort of falls through it, it turns into a string array.
[00:14:45] So looking at the pattern here, what I can see is splitting by empty string seems to be causing us problems. What I'm going to do is grab this test case. And I want to bring it up so that we can see what it actually evaluates to. And I suspect we're gonna have, An extra string of some sort, maybe an empty string at the end.
[00:15:15] Look at that. That little empty string at the end, that little jerk. So something's falling through. It's almost like it's tough to imagine how this works. But yes, there's nothing leftover, but that nothing is it's not necessarily an empty array. It's It's just kind of falling out. So here's what I can do.
[00:15:37] I know this empty string is likely to be the very last tread that's left as everything's been peeled off. That means we're gonna be in this part of the code right? So I want to add one more conditional here. And I'll say if s extends empty string, then return an empty topple otherwise, the remaining nonzero length piece of the string and look at that a little empty string at the end disappeared.
[00:16:11] We're so close, let's grab this little piece here. I could solve this a couple ways. So what I'd hoped to get out of this, what the test case says I should get out of this is an array of strings. And what I'm getting out instead is a tuple of length one containing a string.
[00:16:30] So a quick way I can solve this is down here, I could just say well I could do one more conditional if I want to. Now, this one's tricky because I need to know that I am not a specific string, I need to know that I'm the string type.
[00:16:48] Here's how we can do that, string extends s. So remember, It's like small object fitting into big door, right? So if I have a very specific thing, do you match? Are you any of the strings that are available? Of course that will pass, but I want to flip these around.
[00:17:17] And I wanna say, look, does this general thing, is that equal to the small thing? I wanna flip that around, that's why changing the positions here is important. Do we get it? Then we got that last test. And finally we're down to this one. Does this make sense?
[00:17:39] Does string extend type s? So if s is just the color, the string red like this will fail. The set of all strings, not all the strings are exactly this one thing. So sort of it's less about an equal thing. It's more like a greater than or equal to this extends operator.
[00:18:00] All right z, what are we getting? This is evaluating to, an empty tuple. You know what, I'm gonna leave this as an exercise for you all to continue if you wish. One way to solve it I'll just tell you is you could, I mean there probably is a more elegant way to do this, you could peel this thing, this piece here all the way up to the very top and sort of early terminate.
[00:18:28] If ever you're given an empty string, no matter what you're splitting it by, just return an empty tuple. It's the same thing. I think we would just end up rearranging. But I'll leave you all to poke around with that.