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.
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.