cft

3 Ways To Write Function Overloads With JSDoc & TypeScript

This article will show you how use JSDoc to provide TypeScript type definitions for overloaded functions in JavaScript.


user

Austin Gil

2 years ago | 5 min read

I like TypeScript, but I prefer the JSDoc syntax for writing it. That should be obvious if you’ve read any of my JavaScript articles, especially Get Started With TypeScript the Easy Way.

So far, I haven’t run into any scenarios where I can’t use JSDoc to accomplish the same functionality as TypeScript.

It wasn’t until I needed to implement the type definition for JavaScript function overloads that I seriously started to question that.

What Is Function Overloading?

Feel free to skip this if you already know, but if you don’t let’s first understand what function overloading is.

Function overloads are when you define the same function more than once in order to capture different functionality.

Here’s a contrived example. Let’s say we wanted to create a function called double.
It takes one parameter. If the parameter is a number, it would multiply
it by two and return the result. If it’s a string, it will concatenate
that string to itself and return that.

It’s a silly example, but it might look like this:

function double(input) {
return input * 2}function double(input) {
return input + input
}

Beautiful!

There’s just one problem. JavaScript doesn’t support function overloading. Instead, we’d have to do something like this:

function double(input) {
if (typeof input === 'number') {
return input * 2
}
return input + input
}

A Naive Solution

If we want to write the type definition for this function, it’s a little complicated. We know the input can be either a string or a number, and the output is kind of the same thing.

We could accomplish that with a “union” type. Unions allow us to define a type as being either “this” or “that”. In our case, either a string or a number.

Here we’ll use JSDoc’s @param and @returns keywords to assign the input and output to a union of string and number.

/**
* @param {string | number} input
* @returns {string | number}
*/function double(input) {
if (typeof input === 'number') {
return input * 2
}
return input + input
}

The problem here is that no matter what, when we call our function, we will always get back a union.

What we really want is to get back one specific type based on what the input is. That’s where function overloads come in.

Defining Function Overloads In JSDoc

TypeScript already has documentation dedicated to function overloads. In their example, they show how a simple function can be documented:

function add(a:string, b:string):string;

function add(a:number, b:number): number;

function add(a: any, b:any): any {
return a + b;}

add("Hello ", "Steve"); // returns "Hello Steve" add(10, 20); // returns 30

Notice how the same function is defined three times. Twice for the type definitions, and once for the functionality.

Unfortunately, the same cannot be said for JSDoc.

After a lot of searching, I finally found a solution that seems to
work well enough. We can define a variable whose value is an an
anonymous function. Just above that variable definition, we can use
JSDocs
@type keyword to define the type for that variable, and within that type definition, we can describe our function overloads.

In this case, we want to describe two arrow functions. One that takes an input with a type of number and whose return type is a number, and one that takes an input with a type of string and whose return type is a string:

/**
* @type {{
* (input: number) => number;
* (input: string) => string;
* }}
*/const double = (input) => {
if (typeof input === 'number') {
return input * 2
}
return input + input
}

The above example uses an arrow function. That may not be appropriate for scenarios where scope is a concern. Fortunately, we can accomplish the same with a function expression:

/**
* @type {{
* (input:number) => number;
* (input:string) => string;
* }}
*/const double = function(input) {
if (typeof input === 'number') {
return input * 2
}
return input + input
}

As a result, we will see different type definitions for our functions based on whether the input is a number or a string.

When our input is a number, our function’s type definition shows that it returns a number.

When our input is a string, our function’s type definition shows that it returns a string.

More importantly, we get the desired results for our variable’s type
definition. When the input is a number, our variable is a number.

When the input is a string, our variable is a string.

The syntax for defining function overloads is a bit strange, but it
works well enough in practice. The only caveat I found is that it relies
on assigning the function to a variable. It does not work with
function declarations (ie, function double(input) { /* ... */ }).

To be honest, I can’t think of a scenario where you must
use a function declaration and cannot use a function expression, but if
you really need a solution for that, there is a workaround.

TypeScript also offers generics which you can combine with conditional types
to determine the input type and conditionally return a specific type
based on what the input is. All of that can even work with JSDoc thanks
to the @template keyword (which is not well documented).

Applying that to our example above would look like this:

/**
* @template numOrStr
* @param {numOrStr} input
* @returns {numOrStr extends number ? number : string}
*/function double(input) {
if (typeof input === 'number') {
return input * 2
}
return input + input
}

We define our generic as numOrString,
apply that as the input type, then in the return type, we check whether
the input type extends a number type. If it does, the return value is a
number type. If not, it’s a string type.

Closing

TypeScript is great, and JSDoc is great, but once in a while, the documentation for complex things is sparse.

I wrote this article because I’m sure that I will need to look up how
to do this in the future and I’d rather not go through that whole
treasure hunt again. Instead it can live all in one place here, and
maybe help out other folks like you.

We covered a lot of complex TypeScript things like unions, overloads,
generics, and dynamic types. I hope I was able to explain them in a
clear way. And if you’re new to TypeScript, I’d recommend going to read
my beginners guide.

Thank you so much for reading. If you liked this article, please share it, and if you want to know when I publish more articles, sign up for my newsletter or follow me on Twitter. Cheers!


Originally published on austingil.com.

Upvote


user
Created by

Austin Gil

Over the last ten years, I’ve built projects for award-winning agencies, innovative tech start-ups, and government organizations. Today, I create cool stuff for the web and share what I learn through writing, open-source, YouTube and Twitch, The Function Call, speaking and workshops.


people
Post

Upvote

Downvote

Comment

Bookmark

Share


Related Articles