How to omit or at least make optional the last parameter of function based on generics?
interface Requests {
post: {
data: {
test: number
}
}
patch: {
data: {
test?: number
}
}
get: never
}
const makeRequest = <Method extends keyof Requests>
(method: Method, data: Requests[Method] extends {data: infer Data} ? Data : never)
=> { /* ... */ }
makeRequest('post', { test: 1 }) // that's ok, second parameter is required
Here I should be able to not pass anything as second parameter since data
in patch
has nothing required
makeRequest('patch', {})
makeRequest('patch') // this gives an error: Expected 2 arguments, but got 1
Here the second parameter should be omitted, since it does not have data. If not possible to omit it completely, at least the empty object should not be required
makeRequest('get') // error: Expected 2 arguments, but got 1
Without using function overloads, you would have to conditionally make the second parameter required. The only way to do this would be to destructure a conditionally typed tuple.
A more experienced TypeScript conjurer may find a way to reduce this to only 1 conditional, but alas, here's what I came up with:
const makeRequest = <Method extends keyof Requests>
(...[method, data]:
Requests[Method] extends { data: infer Data }
? Partial<Data> extends Data ? [method: Method, data?: Data] : [method: Method, data: Data]
: [method: Method]) => { /* ... */ }
We still have your original check that tests if it even has a data
property. However, I will note that for this check to work properly, you can't have never
in Requests
(never
is an empty union so it breaks the distributive conditional type).
I would just keep it simple and use undefined
or void
in Requests
:
interface Requests {
post: {
data: {
test: number
}
}
patch: {
data: {
test?: number
}
}
get: undefined // changed never to undefined
}
And finally, the special condition I added, Partial<Data> extends Data
, is just a way to see if everything in Data
is optional (because only then would Partial<T>
be assignable to T
).
It is worth to note that this kind of typing would likely cause type errors when trying to create the implementation for makeRequest
.
Here's what overloads could look like. You could definitely have 3 signatures, but my implementation got too complicated for what I would like...
type KeysThatAreObjects<T> = {
[K in keyof T]: T[K] extends object ? K : never;
}[keyof T];
function makeRequest<
K extends Exclude<keyof Requests, KeysThatAreObjects<Requests>>,
>(method: K): void;
function makeRequest<K extends KeysThatAreObjects<Requests>>(
...args: Partial<Requests[K]["data"]> extends Requests[K]["data"]
? [method: K, data?: Requests[K]["data"]]
: [method: K, data: Requests[K]["data"]]
): void;
function makeRequest(method: keyof Requests, data?: unknown) {
// implementation
}