Parameters, but only sometimes

16 August 2024

Updated: 24 August 2024

Ran into this question today and I thought it would be a nice little example to document:

I have the following function doWork that is generic:

1
function doWork<T>(data?: T): void {
2
console.log(data)
3
}

This function can be called in any of the following ways:

1
// Works
2
doWork<string>('hello') // T is string, data is string
3
doWork() // T is undefined, data is undefined
4
doWork(undefined) // T is undefined, data is undefined;
5
6
doWork<string>() // T is string, data is undefined

In the above usage, we want to make it so that users of this function need to provide the data parameter when calling the function when the type is provided. Now, a simple solution could be to define our function as follows:

1
function doWork<T>(data: T): void

The problem is that it’s a bit ugly for cases where we want to allow T as undefined, because you now have to do this:

1
doWork(undefined)

So the issue here is that we only want to make the data parameter required when T is not undefined, there’s a lot of funny stuff you can try using generics and other weird typescript notation, but simply defining the necessary overloads for our method works:

1
/**
2
* This specifically handles the case where T is undefined
3
*/
4
function doWork<T extends undefined>(): void
5
/**
6
* The data param will be required since T is not undefined here
7
*/
8
function doWork<T>(data: T): void
9
/**
10
* This provideds an implementation that is the same as before while providing a
11
* better interface for function users
12
*/
13
function doWork<T>(data?: T): void {
14
console.log(data)
15
}

Now, having defined those function overloads, we can use the function and it works as expected:

1
// Works
2
doWork<string>('hello') // T is string, data is required
3
doWork() // T is undefined, data parameter is not required
4
doWork(undefined) // T is undefined, specifying data is still okay
5
6
// Error: Type 'string' does not satisfy the constraint 'undefined'
7
doWork<string>() // this overload is not valid

The added benefit of this method is that we can also write the doc comments for each implementation separately which can be a nice way for us to give additional context to consumers. It’s kind of like having two specialized functions without the overhead of having to implement them independently