是否可以在从回调和参数列表创建承诺的函数上创建类型安全?
Is it possible to create type safety on a function that creates a promise out of a callback and a list of arguments?
我定义了一个函数,它接受另一个函数和一个参数列表作为参数来创建一个 promise,如下所示:
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 调用中获得承诺,这些调用都将回调作为第一个参数并且 return 什么都没有(只是调用回调数据代替)。它们具有各种附加参数,并使用各种 return 类型调用回调。
它通常运行良好,但我 运行 有时在编写我以前从未使用过的新 API 调用时感到有些沮丧,因为 Typescript 无法告诉我要传递哪些参数...args
而且我必须花费额外的时间来检查我为 API 构建的类型,以确切知道要传递什么以及如何传递。
在使用中,调用callbackToPromise
的函数都定义了自己的参数,这就是我在接口代码之外使用的,但是在定义新的接口函数时如果能保持类型会更方便那里也很安全。如果我意识到我的 API 类型文件中的输入错误或不完整并且需要更新,则更不容易出错。
有没有办法告诉Typescript“只接受...args
匹配我作为func
传入的函数的参数?
其他详细信息:
我正在调用 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'.
This ended不是由于脚本的 Parameter<T>
部分,而是 .catch
部分的 return 不是 return 值。
Playground based on CRice's second example showing the error
首先我要指出,如果您使用的是 nodejs,则可以为此目的使用内置的 util.promisify
函数,并且它已经带有正确的类型。在浏览器中,您可以使用许多包来达到相同的效果。但是,您也可以修改函数以使用泛型推断承诺类型。
这大量使用 the helper type Parameters<F>
提取类型 F
的参数类型(假设 F
是函数类型)。
它的本质部分是您可以使用 Parameters
辅助类型来提取您的函数将接受的回调的第一个参数的类型。这是您的承诺将解析为的类型。
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
来表示所有剩余参数。
最后 return 输入:
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>
仍然不确定是否理解您的要求,但您可以在下面查看我为 运行 Google API 创建的这个递归通用函数。
该函数有一个异步函数作为参数以及函数的参数(以及与执行相关的附加参数)
这是类型安全的,因为在函数声明中,...args 是 typeof 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",
});
我定义了一个函数,它接受另一个函数和一个参数列表作为参数来创建一个 promise,如下所示:
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 调用中获得承诺,这些调用都将回调作为第一个参数并且 return 什么都没有(只是调用回调数据代替)。它们具有各种附加参数,并使用各种 return 类型调用回调。
它通常运行良好,但我 运行 有时在编写我以前从未使用过的新 API 调用时感到有些沮丧,因为 Typescript 无法告诉我要传递哪些参数...args
而且我必须花费额外的时间来检查我为 API 构建的类型,以确切知道要传递什么以及如何传递。
在使用中,调用callbackToPromise
的函数都定义了自己的参数,这就是我在接口代码之外使用的,但是在定义新的接口函数时如果能保持类型会更方便那里也很安全。如果我意识到我的 API 类型文件中的输入错误或不完整并且需要更新,则更不容易出错。
有没有办法告诉Typescript“只接受...args
匹配我作为func
传入的函数的参数?
其他详细信息:
我正在调用 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'.
This ended不是由于脚本的 Parameter<T>
部分,而是 .catch
部分的 return 不是 return 值。
Playground based on CRice's second example showing the error
首先我要指出,如果您使用的是 nodejs,则可以为此目的使用内置的 util.promisify
函数,并且它已经带有正确的类型。在浏览器中,您可以使用许多包来达到相同的效果。但是,您也可以修改函数以使用泛型推断承诺类型。
这大量使用 the helper type Parameters<F>
提取类型 F
的参数类型(假设 F
是函数类型)。
它的本质部分是您可以使用 Parameters
辅助类型来提取您的函数将接受的回调的第一个参数的类型。这是您的承诺将解析为的类型。
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
来表示所有剩余参数。
最后 return 输入:
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>
仍然不确定是否理解您的要求,但您可以在下面查看我为 运行 Google API 创建的这个递归通用函数。 该函数有一个异步函数作为参数以及函数的参数(以及与执行相关的附加参数) 这是类型安全的,因为在函数声明中,...args 是 typeof 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",
});