The return type of a function will sometimes be determined by the types of the arguments that were passed in. In this example, if we want to allow the second argument here to be either an array of strings, being the task dependencies, or a function, being a task to run itself, then we could implement that in the following way.
We'll do it first without any type-checking at all. The get task function takes a task name, and for the second argument, we'll just use the character X, as we don't know what it's going to be. Then we can do some simple checking or say if type of X is a function. Then we'll return an object literal that has the task name and the property fn, meaning function.
We can do the same thing for the other type that we're expecting. We can check if it's an array. If it is, then we return depths. This is a great example, where we want to ensure that the first argument is always a string, but we want to allow the second one to be either a function or an array.
This is a common scenario and something that TypeScript can actually help us with. Let's first model both of these object literals. We'll say that in the first case, when you pass an array, it will return a task group. This has a task name, which is a string, and it has a depths property, which is an array of strings. The second use case will just be a task, and this will have the fn property, which will be a function.
Now on this first call, we want to return a task group, and on the second one we return a task. Now we've defined our input types, and we've defined our output types. Now if we go to our implementation, we can teach TypeScript about these types.
Task name will always be a string, so we can provide that here. We'll say that X is any, and this function returns any. Now, at this point we're not really any better off than we were before. But here comes the magic. If we just copy this line, and remove this curly brace, this is what's known as a function overload, and it allows us to provide multiple function signatures and their return types.
Let's do that. The first one will say depths, takes an array of strings. When that's the case, it returns a task group. This now covers this use case. Copy the line again. Change this for fn, and save function. This case will return a task.
That in a nutshell is function overloads. Above your implementation, you can provide separate signatures that specify different types of inputs and different types of outputs. The benefit, then, is that the compiler and your IDE can be much smarter about your code.
If we think back to the previous example, if we tried to access taskone.fn, immediately you can see we have the red squigglies again, and an error saying property fn does not exist on type task group. This is a good example of how TypeScript can still give you deep code insight even when you have dynamic functions such as these that can return different types.