I have defined a function that takes another function and a list of arguments as parameters to create a promise, like so:
async callbackToPromise (func: Function, ...args: any[]): Promise<any> {
// Immediately return if the function argument is undefined, to avoid errors.
if (func === undefined) {
console.warn('Function undefined in callbackToPromise.')
return await Promise.reject(
new Error('Function undefined in callbackToPromise.')
)
}
const call = new Promise((resolve, reject) => {
func((resolveStr: unknown) => {
if (resolveStr !== undefined) {
return resolve(resolveStr)
} else {
return reject(new Error('No data returned'))
}
}, ...args)
})
return await call
}
I use this function to get promises out of a not insignificant variety of API calls in my environment that all take a callback as the first parameter and return nothing (just calling the callback with data instead). These have a variety of additional parameters and call the callback with a variety of return types.
It generally works well, but I run into some frustration sometimes when writing up new API calls that I haven't used before as Typescript can't tell me which parameters to pass for ...args
and I have to spend extra time checking against the typing I built for the API to know exactly what to pass in and how.
In use, the functions that call callbackToPromise
all define their own parameters and that's what I use outside of the interface code, but it would be more convenient when defining a new interface function if I could keep type safety there, too. And be less error prone if I realize a typing in my API type file is wrong or incomplete and needs to be updated.
Is there any way to tell Typescript "only accept ...args
matching the parameters of the function I pass in as func
?
Update: more details.
I am calling callbackToPromise
with black box functions I have no access to, for example one with a signature like this (which lives as a method of window.external
):
RemoveProblem: (
callback: Function,
prid: number,
stopDate: string,
isApproxDate: boolean,
reason: TProblemRemoveReason
) => void
An example of how I am using it in my code (part of a longer function definition):
const result: number = await this.helpers
.callbackToPromise(
this.wrappedWindow.external.RemoveProblem,
prid,
stopDate,
isApprox,
reason
)
.catch((error: Error) => {
console.error(`Error removing problem: ${error.message}`)
})
What I would like, ideally, would be for callbackToPromise
to give a type error on compile if I try to pass parameters that don't match the function I pass it as the first argument.
Update2: I have a partial answer based on @CRice 's answer below, but get a weird issue when I try to actually call the function with an await
, Type 'number | void' is not assignable to type 'number'. Type 'void' is not assignable to type 'number'.
I could make result
in this case not typed and then it passes the error on to everywhere I use result
assuming it is a number, unless I add a check for undefined.
If I don't type result
at all, though, it implicitly becomes a number
. Weird.
Playground based on CRice's second example showing the error
CodePudding user response:
First I'll point out that if you are using nodejs, you can use the built in util.promisify
function for this purpose and it comes with correct types already. In the broswer, there will be plenty of packages you can use for the same effect. However, you can also modify your function to infer the promise type by using generics.
This makes heavy use of the helper type Parameters<F>
which extracts the type of the parameters of the type F
(assuming F
is a function type).
The essential part of it is that you can use the Parameters
helper type to extract the type of the first parameter of the callback that your function will accept. This is the type that your promise will resolve to.
const callbackToPromise = async <A extends any[], F extends (CB: (result: any) => any, ...args: A) => any>(func: F, ...args: A): Promise<Parameters<Parameters<F>[0]>[0]> => {
// Immediately return if the function argument is undefined, to avoid errors.
if (func === undefined) {
console.warn('Function undefined in callbackToPromise.')
return await Promise.reject(
new Error('Function undefined in callbackToPromise.')
)
}
const call = new Promise((resolve, reject) => {
func((resolveStr) => {
if (resolveStr !== undefined) {
return resolve(resolveStr)
} else {
return reject(new Error('No data returned'))
}
}, ...args)
})
return call
}
There is a bit to unpack there, but here is what is going on:
A extends any[]
declares a genericA
which is an array of any other types. This will be used in a moment to represent the type of the all but the first arguments tofunc
.F extends (CB: (result: any) => any, ...args: A) => any
declares an additional genericF
, which is a function that accepts a callback as its first parameter and then uses the earlier genericA
to represent all the remaining parameters.
Finally the return type:
Promise<Parameters<Parameters<F>[0]>[0]>
just says that the promise will resolve to the type of the first argument of the callback that the functionF
accepts.
Using that definition, you seem to get the correct inferences when using it:
const numericCallback = (cb: (v: number) => void, num: number): void => {
cb(num);
}
const promisifyNumericCallback = callbackToPromise(numericCallback, 56) // This is inferred as a Promise<number>