Being able to handle the ability to call a single function with variable inputs is really nice. Sometimes we need a function to do different things based on the parameters given. When doing this be sure to consider code smells like I detailed in my last email.
Let's look at a simple example we're all probably familiar with: optional parameters.
Here we see three uses of the slice method. Slice returns a portion of an array. It has two parameters, each optional. The first is the index to start from, the second is the index to end at.
Straightforward.
We can also use default parameters. In the above, you can consider the first parameter to have a default value of 0. (slice actually just has that first parameter marked as optional, but the effect is the same)
A reasonable signature for this may be the following:
This gives us all kinds of flexibility in creating signatures in TypeScript that give us good intellisense and clues as to how to use the method. Just look at the intellisense that Visual Studio code gives us for the array.slice method.
This is great, but there are some scenarios that optional and default parameters just don't cover.
Let's look at a different scenario.
What if we are writing a method that creates database connections? We have two scenarios. First, we just receive a connection string with an optional timeout. Second, we get an IP address, port number, username, password, and optional timeout. Our scenarios look like this:
So how can we solve this?
Option 1: We could try to use optional and default parameters here. Something like this:
But WOW look how messy and even misleading that is. Not expressive at all.
Option 2: different methods
That's ok, but it really feels clunky. If we have more scenarios, it gets worse.
Option 3: Parameter Object. This is commonly used on JavaScript for just this scenario.
This works just fine, but it doesn't communicate through the signature what the parameters are, and the various configurations. You can, with TypeScript give the parameter object a defined shape, but it's pretty complex based on our possible configurations, and is just less expressive. I personally think parameter objects can be ok, but they are possibly overused. Destructuring with an interface is kind of a better parameter object. You can check out a blog on that here. Sometimes this method fits the bill, but let's look at another option:
Function Overloading
With function overloading, we can create different signatures of a function, making each expressive, giving us IntelliSense, and ultimately a single method handles all the calls. Let's look at the basics of this, and then we'll solve our problem.
Here's the basic syntax in TypeScript:
The way this works is we give each signature without a body, and then we provide a method that is a superset of ALL signatures and actually put our function body in there. There can only be ONE implementation. It's the last one. We can't have 2 different implementations. Inside our method body we have to branch based on the parameters, like so:
The signatures show up in TypeScript intellisense, but the third signature, the ACTUAL implementation, doesn't actually show up. So TypeScript would allow only strings and numbers, not other types.
Now that we've seen how to do this, let's solve our problem above:
Here's the correct syntax. Notice that the final signature is the actual implementation, and it's a superset of the first two. Since the types line up, we're able to use specific types. Often times you use the any type or a union type when the types for a specific parameter vary.
Notice that I named the first parameter just p1. This is because it's either a connection string OR an IP Address. Again, this won't show up in the IntelliSense when I try to call this method. So in the implementation, I'll determine which variation of my signature was called, and take the appropriate action. Possibly even assigning p1 to either a local ipAddress or connectionString variable. That really just depends on the implementation.
A start of a possible implementation for the actual method would be this:
This gives us the basic branch based on which signature the user selected. Again, the great thing here is that TypeScript does the checking for us at compile time. So if the 3rd argument is a string, then we know which signature the caller used. Note: don't use the typical !!username method to determine if something was passed in. Use the type as your branching criteria.
This method isn't the solution to every scenario. As we get multiple scenarios that are very complex, parameter objects make more sense. Also, sadly we can't use default values in overloads.
But getting comfortable with overloading functions just gives you another simple way to handle a somewhat common scenario in a simple fashion. I hope you find it useful.
Check out our 100 Algorithms challenge and all our courses on JavaScript, Node, React, Angular, Vue, Docker, etc.
Happy Coding!
Enjoy this discussion? Sign up for our newsletter here.
Visit Us: thinkster.io | Facebook: @gothinkster | Twitter: @gothinkster
Top comments (0)