【问题标题】:Is it possible to create type safety on a function that creates a promise out of a callback and a list of arguments?是否可以在从回调和参数列表中创建承诺的函数上创建类型安全?
【发布时间】:2021-11-18 00:05:29
【问题描述】:

我已经定义了一个函数,它接受另一个函数和一个参数列表作为参数来创建一个 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 调用中获取承诺,它们都将回调作为第一个参数并且不返回任何内容(只是使用数据调用回调)。它们有多种附加参数,并以多种返回类型调用回调。

它通常运行良好,但有时我在编写我以前没有使用过的新 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'. 这最终不是由于Parameter&lt;T&gt; 部分脚本,但 .catch 部分的返回没有返回值。

Playground based on CRice's second example showing the error

【问题讨论】:

  • Don't use any unless you are in the middle of migrating from vanilla JS to TSany 在任何使用的地方都有效地禁用了打字稿。
  • 不确定是否了解您的要求。你有一个预期结果的例子吗?因为据我了解,您想将函数 (func) 转换为 promise 吗?或者你只是想执行函数并返回它的结果?
  • @Mulan 我可以毫无问题地更改为...args: unknown[],但是将返回类型更改为Promise&lt;unknown&gt; 会导致我调用它的任何地方告诉我'未知不能分配给(无论类型)'。我知道回调将接收什么类型的参数,但不确定如何指定。

标签: typescript promise callback


【解决方案1】:

仍然不确定是否理解您的要求,但您可以在下面查看我创建的用于运行 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;
};

调用tis递归函数的例子是

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",

    });


【讨论】:

  • 感谢您的回答!我发现&lt;&gt; 符号仍然很难阅读,但我认为这可能会解决问题。 Crice 上面的回答直接解决了这个问题,我已经在使用它了。
【解决方案2】:

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

这大量使用了the helper type Parameters&lt;F&gt;,它提取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) =&gt; any, ...args: A) =&gt; any 声明了一个额外的泛型 F,它是一个接受回调作为其第一个参数的函数,然后使用早期的泛型 A 来表示所有剩余的参数。

最后是返回类型:

  • Promise&lt;Parameters&lt;Parameters&lt;F&gt;[0]&gt;[0]&gt; 只是说 promise 将解析为函数 F 接受的回调的第一个参数的类型。

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

const numericCallback = (cb: (v: number) => void, num: number): void => {
    cb(num);
}

const promisifyNumericCallback = callbackToPromise(numericCallback, 56) // This is inferred as a Promise<number>

Playground Link

Playground Link 2

【讨论】:

  • 不幸的是,我的用例不涉及 util.promisify 工作的那种功能。我的交互是与 EMR 的自定义 API 进行交互,它可以调用提供的回调,这取决于我执行的函数,从无到有,一个代表成功/失败代码的数字,一个字符串结果,一个 JSON 对象......让我看看你的例子是否适合我,我会回复你。
  • 也许我不清楚我到底想做什么。我认为Paramater&lt;T&gt; 可能会有所帮助,但我根本不明白,你给我的链接完全没有解释,并且有六个对我来说毫无意义的例子,所以我正在做更多的研究。在将提供的代码复制到我的项目中代替我的原始函数后,我立即收到了十几个编译错误(主要是type 'void' is not assignable to type 'x')....
  • ... 我将作为第一个参数提供的示例函数签名:RemoveProblem: (callback: Function, prid: number, stopDate: string, isApproxDate: boolean, reason: TProblemRemoveReason) =&gt; void。 “回调”函数接收RemoveProblem 生成的任何结果,RemoveProblem 是我无法访问的黑匣子。 callbackToPromise 允许我调用 RemoveProblem 作为一个承诺,而不是为每次使用它创建一个回调函数。但是尽管定义了接口,但我没有通过prid, stopDate, isApproxDate, reason(等)的类型安全。
  • 也许您可以编辑您的问题以涉及您的确切用例,我可以相应地进行调整。 FWIW 似乎这个解决方案 does 为该示例签名提供了您想要的类型安全性,您能否再举一个破坏它的示例?我在答案底部编辑了一个额外的游乐场链接,看看。
  • 我刚刚用更多细节更新了这个问题。我浏览了您的游乐场链接,令我印象深刻的一件事是它不应该解析为 Promise&lt;never&gt;,对于 RemoveProblem,它应该是 Promise&lt;number&gt;,但我使用它的其他功能各不相同。
猜你喜欢
  • 2022-01-25
  • 2017-04-26
  • 2016-08-19
  • 1970-01-01
  • 2017-08-04
  • 1970-01-01
  • 1970-01-01
  • 2016-05-09
相关资源
最近更新 更多