Function overloading in TypeScript

As more and more companies migrate from Java to JavaScript for UI and API testing, I decided to study it too. After all, even this blog is powered by React. One of the features I sometimes used in Java is function overloading, which I thought wasn't possible in JavaScript. However, as I've recently learned, TypeScript adds this functionality. Well, kind of.

JavaScript code on a screen

Photo by Pixabay

The main difference compared to Java is that an overloaded function still only has one implementation. Yet, it can have multiple signatures. So, while this Java-like code is illegal:

type StorageItem = string

class DataStorage {
    private data: StorageItem[] = ['item1', 'item2', 'item3', 'item4', 'item5']

    get(): StorageItem {
        return data.shift()
    }

    get(n: number): StorageItem[] {
        return this.data.slice(0, n)
    }
}

This code is fine:

type StorageItem = string

class DataStorage {
    private data: StorageItem[] = ['item1', 'item2', 'item3', 'item4', 'item5']

    get(): StorageItem

    get(n: number): StorageItem[]

    get(n = 1): StorageItem | StorageItem[] {
        // I'm using the exclamation point to suppress a type check
        // Because shift() can return either 'string' or 'undefined'
        // However, in this code snippet, we will always get a 'string'
        // So let's keep it simple :)
        if (n === 1) return this.data.shift()!

        return this.data.splice(0, n)
    }
}

const storage = new DataStorage()

const singleItem = storage.get()
const multipleItems = storage.get(3)

Now TypeScript and our code editor know that for plain get() calls we will get back a single StorageItem object, while for get(n) - an array of StorageItem objects. Without the overloading, singleItem and multipleItems constants would both be of type StorageItem | StorageItem[] because that's what the actual function returns. Since strings and arrays are different objects with different methods, we can't treat them the same later in the code. We'd need to handle this ambiguous return type after each get() call. In this regard, function overloading is helpful.

However, this comes with its pitfalls. Consider this code:

function size(value: string): string

function size(value: any[]): number

function size(value: string | any[]) {
    // Return 'n words' for string inputs
    if (typeof value === 'string') return `${value.split(' ').length} words`

    // Or the array's length for array inputs
    return value.length
}

Here, the function accepts either a string or an array. It has to perform an explicit type check to decide what that value is and what to do with it. I suspect that code like this can quickly become messy in a real project solving real problems.

Hence, I believe it's easier and clearer to create separate functions with different names than to use function overloading in TypeScript. Yes, it helps with code hints and type checks, but having one body for multiple functions is concerning. It makes them harder to understand and test.