TypeScript Fundamentals, v3 Functions & Function Overloads
Transcript from the "Functions & Function Overloads" Lesson
>> It's time for us to dive a little bit more deeply into functions and there were a couple of these early topics we needed to tackle first. The union intersection operators for sure and these named types, right? Like interfaces, type aliases, so now we're ready for this and we already talked about argument types and return types.
[00:00:24] Now we're gonna go deeper, so we're gonna talk about defining Callable types, by that I mean, interfaces that describe functions. Interfaces are type aliases rather, that describes something that can be invoked, or something that can be constructed using the new keyword. We're gonna talk about function overloads and I won't even attempt to give you a sneak preview of what that is because it's, if you've never seen it before, it's kind of a wild concept.
[00:00:58] We'll talk about typing this, If your function has a special need for making sure that whenever it's invoked, it's, this scope is a specific thing. And then finally I think we covered this a little bit, but we'll touch one more time on function return types, and best practices, so let's jump in.
[00:01:24] Type aliases and interfaces, they both provide some capacity for defining a call signature. So this is an interface we're used to seeing instead of this thing that I've highlighted, we're used to seeing a bunch of different property names, right? Instead we have parentheses and within them what appears to be an argument list, and then a colon, and then a return type.
[00:01:48] So, this is a callable thing, in a type aliases form, this is what it would look like and note that we're using a fat arrow here kind of like an arrow function, whereas up here we're using a colon. These are totally equivalent, these top two and we can see here, we benefit when we do something like this to define functions stored on variables, we benefit from having defined these call signatures.
[00:02:21] We don't need to have type annotations on function arguments. Usually you always need to, well that's not gonna use usually an always in same sentence, but with regular function declarations, you must include argument types because there's no way to infer them. In this case, all of the type information you need is on the left side of the assignment operator.
[00:02:47] So we know this is a number, we know this is a number and we know we should be returning a number, you don't need to state that information again. This is great for things like callbacks, right? Where it just saves you from having to add more and more type annotations.
[00:04:02] It returns undefined and then here's another one invoke in five seconds, and its return type is void. So, note that this little arrow function here, it happens to return something, why? Because, although we hardly ever use it Array.push actually returns a number that I mean, I don't even know what this number represents.
[00:04:32] We think of it almost like an entirely state modifying method that doesn't really return something useful, it happens to return something. So, the void case is quite happy with this, the undefined cases not, so there's a difference between saying the return value of this function should be ignored.
[00:04:56] Versus, I promise I will always return undefined, a void callback here you're fun, returning whatever you like, you will be encouraged to not use that value within the body of this function. But it's fine if you happen to return something, whereas in this case up here, you may not return something versus here it's like do what you like.
[00:05:22] Void is happy with whatever because we're not going to use it, right? So, that's the difference and important difference between void and undefined, void should only appear as a function return type. You should not be using it anywhere else because what it means is the return value of this function should be Ignored and left unused.
[00:05:44] Let's talk about construct signatures for a moment, this is another area that is sort of rare, cuz oftentimes we just define classes which kind of have this sort of functionality built in. But we have the ability to do this, so here's an example of a Date constructor, and it looks just like a call signature, except we've added this new keyword in front of it, that's it.
[00:06:10] So now, if I were to create something that I'm calling, my Date constructor, set it equal to the date, class itself, not a new instance of date. Now you see the TypeScript perfectly happy to let me instantiate, you probably won't come across this very often, but if you see it, I want you to know what you're looking at.
[00:06:29] This, you should come across function overloads, so, because this is a Kind of abstract concept, I wanna motivate the existence of this thing, function overloads with a potential use case. So, let's imagine a situation where we wanted to create a function that allows us to register what we're calling a main event listener where that means, the primary event of whatever DOM element we appear to be referring to.
[00:07:03] So, in the i frame case, if we're past an i frame, I want us to be setting up a post message callback, like we're communicating with the i frame, right? That's the main event you care about for an i frame, if you don't believe me, let's pretend that it is and then form would probably want submit, right?
[00:07:26] That's a big, meaningful event that's associated with the form, so we could give it a shot by creating two types that represent sort of our event listeners, one that receives form data, one that receives a message event and this is for the i frame. And we could say, all right, so my first element here my first arg it's one of these two types of elements, it's either the form or the i frame and then here we've got the handler for the form or the i frame, right?
[00:08:01] We certainly can handle both cases and I can get the zeroeth i frame on the page and I can try to handle this event. So here's the i frame, something's already looking a little bit off here, right? Because we're not getting any type hint, we're just getting an any, so that's not good, we're allowing a lot of possibilities here.
[00:08:28] Basically, we don't have any sense of saying the first argument passed to the function has any effect on which second argument we should accept. So, to put this more concisely, what we want, is when we get a form to strictly be dealing with submit, and when we get an i frame to strictly be dealing with post message, not a combination of the two, that might not make any sense.
[00:09:01] So, function overloads help us solve this, and effectively what we're doing here is we're saying I have one implementation in my function, but I wanna define types for two specific entry points. One that does the form stuff with the right element and the right handler, and one that does the i frame stuff with the right element and the right handler.
[00:09:29] This ensures that I have two specific prescribed ways of doing things and I'm not left with all of the flexibility that I really don't want to handle such as this in combination with that, that would be sort of like crossing our wires. So, we can see here if we get our frame and get a form, this case works for handling the frame, we can see we have type information here, there's our message event and then in the form case, this works as well.
[00:10:07] Let me show you what a failing case look like, basically, like let me show you what we can't do anymore, and that would be something like this. Actually a better way to Illustrate this is just to show you what the autocompletes look like. So, normally we just get autocompletes like this and when we open this up we would just see like, here's what you should type, but, look, we got one of two and two of two, we kind of have two options.
[00:10:36] For what we can do here, the form way, or the i frame way and note that we don't actually, we're not offered the arguments that are described on lines 14 and 15 of this piece of code. No one's showing us In a union type, right? Not for option one, not for option two, right?
[00:11:02] It's one or the other, we have two choices for the function, we're not allowed to mix. And what we're seeing here is that we can see this, we can see this, this here is effectively hidden, that's part of what is allowing us to do this. It basically means that this is sort of what's invoked behind the scenes, we can make this whatever it needs to be in order to handle all of the cases we care about.
[00:11:29] But we can have very specific entry points that allow us to avoid handling these combinations of things that we have no intent of allowing. So, we would call these two heads of the function and then this often is referred to as an implementation of the function and this is the only thing that has the braces, right?
[00:11:51] So, an important thing to think about as you consider using function overloads, is that the implementation has to be compatible with the heads. So, the problem I've created here is I think I just deleted the first argument for this middle bit here and basically what the implementation is the error message that we're getting is that there's just no way for all of these to line up.
[00:12:23] They kind of need to be specific case one, specific case two for the heads, and then an implementation that's general enough to handle anything that might come in. But in this case it's sort of broken, right? Meaning, we're only accepting one element here, it's, we need something else, if we were to, for example, just say look, I'm gonna take another argument, And we need to see the fact that, we have this type here, we need to allow for a possibility that an i frame element might come through.
[00:13:01] So, this almost, it has to be general enough that it could type check against any combination of the head above it. So, these are almost like specific cases that live within the type described by this hidden piece. It's a really powerful tool that lets you have greater opportunities for code reuse, because you can have something that is very carefully designed with specific entry points, while having some very general shared logic that backs all of those function signatures.