是否有可能在一个函数上创建类型安全,该函数从回调和参数列表中创建一个承诺?



我定义了一个函数,它接受另一个函数和一组参数作为参数来创建一个承诺,如下所示:

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
}

在我的环境中,我使用这个函数从各种各样的API调用中获得承诺,这些API调用都以回调作为第一个参数,并且不返回任何东西(只是用数据调用回调)。它们具有各种附加形参,并使用各种返回类型调用回调函数。

它通常工作得很好,但有时我在编写以前没有使用过的新API调用时遇到一些挫折,因为Typescript无法告诉我为...args传递哪些参数,我必须花费额外的时间检查我为API构建的类型,以确切地知道要传递什么以及如何传递。

在使用中,调用callbackToPromise的函数都定义了自己的参数,这就是我在接口代码之外使用的参数,但是在定义一个新的接口函数时,如果我也可以在那里保持类型安全,那将更方便。如果我意识到我的API类型文件中的输入是错误的或不完整的,需要更新,则更容易出错。

有没有办法告诉Typescript"只接受与我传入的func函数的参数相匹配的...args"?

额外的细节:

我正在使用我无法访问的黑盒函数调用callbackToPromise,例如具有这样签名的函数(作为window.external的方法存在):

RemoveProblem: (
callback: Function,
prid: number,
stopDate: string,
isApproxDate: boolean,
reason: TProblemRemoveReason
) => void

我如何在我的代码中使用它的一个例子(一个较长的函数定义的一部分):

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}`)
})

理想情况下,我想要的是callbackToPromise在编译时给出类型错误,如果我试图传递与我作为第一个参数传递的函数不匹配的参数。

旁注:当我最初试图实现CRice下面的答案时,我遇到了一个问题,当我尝试实际调用await函数时,Typescript会说Type 'number | void' is not assignable to type 'number'. Type 'void' is not assignable to type 'number'.这最终不是由于脚本的Parameter<T>部分,而是由于.catch部分的返回不返回值。

Playground基于CRice的第二个示例,显示了错误

首先我要指出的是,如果你正在使用nodejs,你可以使用内置的util.promisify函数来实现这个目的,它已经提供了正确的类型。在浏览器中,您可以使用许多包来实现相同的效果。然而,你也可以修改你的函数,通过使用泛型来推断承诺类型。

这会大量使用helper类型Parameters<F>,它提取类型F的参数类型(假设F是一个函数类型)。

它的关键部分是您可以使用Parametershelper类型来提取函数将接受的回调的第一个参数的类型。这是您的承诺将解析到的类型。

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
}

这里有一点需要解包,但这里是正在发生的事情:

  • A extends any[]声明了一个泛型的A,它是任何其他类型的数组。这将用于表示func参数的类型,除了第一个参数。
  • F extends (CB: (result: any) => any, ...args: A) => any声明了一个额外的泛型F,它是一个函数,接受回调作为它的第一个参数,然后使用之前的泛型A来表示所有剩余的参数。

最后返回类型:

  • Promise<Parameters<Parameters<F>[0]>[0]>只是表示承诺将解析为函数F接受的回调的第一个参数的类型。

使用该定义,当使用它时,您似乎得到了正确的推断:

const numericCallback = (cb: (v: number) => void, num: number): void => {
cb(num);
}
const promisifyNumericCallback = callbackToPromise(numericCallback, 56) // This is inferred as a Promise<number>

操场联系

游乐场环线2

仍然不确定是否理解您的需求,但是下面您可以检查我创建的用于运行Google api的递归泛型函数。该函数有一个async函数作为参数以及函数的形参(以及与执行相关的其他参数)。这是类型安全的,就像函数声明一样,…args的类型为U[, args形参也是U。

import { GaxiosResponse } from "gaxios";
const execGoogleApiRecurrent = async <
R,
T extends Record<string, any>,
U extends Record<string, any>,
K extends Record<string, any>
>(
rootApi: R,
funct: (...args: U[]) => Promise<GaxiosResponse<T>>,
key: keyof T,
previous: K[],
nextPageToken: string | undefined,
args: U
): Promise<K[]> => {
if (nextPageToken) {
args = {
...args,
pageToken: nextPageToken,
};
}
const calll = await funct.call(rootApi, args);
previous = [...previous, ...calll.data[key]];
if (calll.data.nextPageToken) {
previous = [
...(await execGoogleApiRecurrent(
rootApi,
funct,
key,
previous,
calll.data.nextPageToken,
args
)),
];
}
return previous;
};

调用递归函数的例子是

const builds = await execGoogleApiRecurrent<
admin_directory_v1.Resource$Resources$Buildings,
admin_directory_v1.Schema$Buildings,
admin_directory_v1.Params$Resource$Resources$Buildings$List,
admin_directory_v1.Schema$Building
>(this._api, this._api.list, "buildings", [], undefined, {
auth: this._auth,
customer: "ddds",
});

最新更新