我正在考虑编写一些“承诺”的函数等效项,但无法理解如何获取除那些看起来像回调的参数之外的所有参数(它们通常有 2 个参数,看起来像这样:
(error, result) => void
。
这些函数位于不同的模块中,它们在 .d.ts 文件中定义。这意味着获取这些参数非常有用,因为其中一些参数作为私有类型位于模块内部。
此类函数类型的示例(您可以注意到回调可以在任何地方,并且可以有多个回调):
type FuncLoad1 = (arg1: string, arg2: string, callback: (error: string | null, result: string | null) => void) => void;
type FuncLoad2 = (arg1: string, callback: (error: string | null, result: string | null) => void, arg2: string) => void;
type FuncLoad3 = (arg1: string, callbackInProgress: (progress: number) => void, arg2: string, callbackOnCompleted: (error: string | null, result: string | null) => void) => void;
是否可以从上面自动获取该类型?
(arg1: string, arg2: string) => Promise<string | null>
正如您所注意到的,可以有任意数量的回调,并且它们可以在任何地方。处理后的类型中仅剩下非函数的参数。
不幸的是,没有合理的方法来实现上述目的。
编写一个类型函数非常容易,该函数从 tuple 类型中删除所有类似回调的参数:
type TupleExclude<T extends any[], U, A extends any[] = []> =
T extends [infer F, ...infer R] ? TupleExclude<R, U, F extends U ? A : [...A, F]> : A;
然后您可以拥有一个实用程序类型,该实用程序类型返回该参数列表中任何回调的第二个参数的
Promise
:
type Promisify<T extends (...args: any) => void> =
(...args: TupleExclude<Parameters<T>, (e: any, r: any) => void>) =>
Promise<Parameters<Extract<Parameters<T>[number], (e: any, r: any) => void>>[1]>;
type PFL1 = Promisify<FuncLoad1>;
// type PFL1 = (args_0: string, args_1: string) => Promise<string | null>
type PFL2 = Promisify<FuncLoad2>;
// type PFL2 = (args_0: string, args_1: string) => Promise<string | null>
type PFL3 = Promisify<FuncLoad3>;
// type PFL3 = (args_0: string, args_1: string) => Promise<string | null | undefined>
这与您的要求相当接近,除了
PFL3
的返回类型。这里的主要问题是,没有简单的方法来区分 (progress: number) => void
和 (error: string | null, result: string | null) => void
类型来说明第二个是您关心的“回调”。前者仅声明单个参数,但较少参数的函数可以分配给较多参数的函数,这使得它们兼容。
有可能想象真正挖掘类型系统以尝试找到该示例的“回调”,但是如果您有多个两个参数回调会发生什么?您不能使用 error
和
result
等参数的 names。参数名称故意在类型系统中不可观察(请参阅 microsoft/TypeScript#55736)。但即使你可以做到这一点,你也应该备份并尝试想象实现一个以这种方式工作的
promisify
函数。
大概您想编写一个以下形式的函数
declare function promisify<T extends (...args: any)=>void>(f: T): Promisify<T>;
可以让你打电话
declare const: fl3: FuncLoad3;
const pfl3 = promisify(fl3);
pfl3("abc", "def");
嗯,它需要一个运行时实现。该实现无法访问 TypeScript 的类型系统,该系统在 JavaScript 代码运行时已被“擦除”。所以它必须以某种方式使用参数 fl3
和
"abc"
来调用 "def"
。它如何知道 fl3
需要多少个回调以及将它们放在哪里?类型 (arg1: string, callbackInProgress: (progress: number) => void, arg2: string, callbackOnCompleted: (error: string | null, result: string | null) => void) => void;
在运行时不存在,并且运行时的函数对象不会公开太多关于它所采用的参数的信息(有 length
函数的属性,但唯一能告诉你的是所需的数量参数)。运行时没有足够的信息供您执行任何操作。这就是所问问题的答案;这在类型系统中实际上是不可能的,并且在运行时也是完全不可能的。
如果你想将其转换为可能的东西,你必须想出某种方法来告诉你的函数回调应该去哪里。最简单的事情就是声明:将有一个回调,并且将在最后,并且它将期望结果作为第二个参数。如果你这样做了,事情就会直接进行:
function promisify<A extends any[], R>(f:
(...args: [...A, (error: any, result: R) => void]) => void
): (...args: A) => Promise<R>;
function promisify(f: Function) {
return (...args: any) => new Promise((resolve, reject) => {
f(...args, (err: any, result: any) => {
if (err == null)
resolve(result);
else
reject(err);
})
})
}
function funcLoad(arg1: string, arg2: string,
cb: (error: string | null, result: string | null) => void) {
setTimeout(() => cb(null, arg1 + " " + arg2), 1000);
}
const p = promisify(funcLoad);
// const p: (arg1: string, arg2: string) => Promise<string | null>
async function foo() {
const val = await p("abc", "def");
console.log(val?.toUpperCase());
}
foo(); // "ABC DEF"
您甚至可以编写一些内容,其中
promisify
采用某种选项对象来更改回调的数量和/或位置并查阅它。但是你也可以将你的函数包装成符合“single-callback-at-end”形式:
// FuncLoad3, not the expected form
function funcLoad3(arg1: string,
callbackInProgress: (progress: number) => void, arg2: string,
callbackOnCompleted: (error: string | null, result: string | null) => void) {
setTimeout(() => {
callbackInProgress(123);
setTimeout(() => callbackOnCompleted(null, arg1 + " " + arg2), 1000);
}, 1000);
}
// just wrap it
const wrapFunc3 = (arg1: string, arg2: string,
cb: (error: string | null, result: string | null) => void) =>
funcLoad3(arg1, (n) => console.log(n), arg2, cb);
// now it works
const p3 = promisify(wrapFunc3);
async function bar() {
const val = await p3("ghi", "jkl");
console.log(val?.toUpperCase());
}
bar(); // 123, then "GHI JKL"
但是所有这些都偏离了所提出的问题,所以我不会详细介绍其工作原理。Playground 代码链接