【问题标题】:Method chaining with async in Typescript (just found out a solution)Typescript 中使用异步的方法链接(刚刚找到了解决方案)
【发布时间】:2020-10-31 03:12:17
【问题描述】:

我正在编写一个模块,旨在在将查询调用到数据库之前准备查询。 vanilla javascript 中的代码运行良好,但是当我尝试用 Typescript 编写它时,出现错误:Type of 'await' operand must either be a valid promise or must not contain a callable 'then' member

我的 Javascript 代码:

class QueryBuilder {
  constructor(query) {
    this.query = query;
  }

  sort(keyOrList, direction) {
    this.query = this.query.sort(keyOrList);
    return this;
  }

  skip(value) {
    this.query = this.query.skip(value);
    return this;
  }

  limit(value) {
    this.query = this.query.limit(value);
    return this;
  }

  then(cb) {
    cb(this.query.toArray());
  }
}

打字稿中的代码:

class QueryBuilder {
  public query: Cursor;
  constructor(query: Cursor) {
    this.query = query;
  }

  public sort(keyOrList: string | object[] | object, direction: any) {
    this.query = this.query.sort(keyOrList);
    return this;
  }

  public skip(value: number) {
    this.query = this.query.skip(value);
    return this;
  }

  public limit(value: number) {
    this.query = this.query.limit(value);
    return this;
  }

  public then(cb: Function) {
    cb(this.query.toArray());
  }
}

我如何调用这些方法:

const query = await new QueryBuilder(Model.find())
    .limit(5)
    .skip(5)

希望有人可以帮助我。提前致谢。

*更新: 我从buitin Promise 扩展了QueryBuilder 类,然后用QueryBuilder.prototype.then 覆盖了then 方法。代码现在是可执行的,但我并没有真正理解构造函数中的super(executor)。它需要executor(resolve: (value?: T | PromiseLike<T> | undefined) => void, reject: (reason?: any) => void): void,所以我只是简单地创建了一个哑执行器。对代码有什么影响?

class QueryBuilder<T> extends Promise<T> {
  public query: Cursor;
  constructor(query: Cursor) {
    super((resolve: any, reject: any) => {
      resolve("ok");
    });
    this.query = query;
  }

  public sort(keyOrList: string | object[] | object, direction?: any) {
    this.query = this.query.sort(keyOrList);
    return this;
  }

  public skip(value: number) {
    this.query = this.query.skip(value);
    return this;
  }

  public limit(value: number) {
    this.query = this.query.limit(value);
    return this;
  }
}

QueryBuilder.prototype.then = function (resolve: any, reject: any) {
  return resolve(this.query.toArray());
};

【问题讨论】:

  • 我认为不需要在 query 初始化语句中添加 await。 QueryBuilder 类初始化操作没有返回任何promise,因此报错。
  • 即使在从查询实例化语句中删除 await 之后,您仍然应该能够 chain 操作。
  • @DhruvShah this.query.toArray() 可能会返回一个 Promise,因此,您必须 await 它的结果。链接本身也不是问题。 OP 代码的重点是实际执行异步查询并在与await 一起使用时等待其结果。
  • 看在上帝的份上,如果你的类不符合 Promises/A+ 规范(它不符合),请不要给你的类一个 .then 方法。你不想引爆another flame war
  • @vmtran 不,我所说的完全不是针对 Typescript 的。是的,您可以实现 .then 函数,使其不符合 Promisea/A+ 但 async 仍然能够处理它,但这是一种可怕的做法。如果有人真的试图直接使用你的.then 方法,他们最终会得到一个Zalgo。如果他们可以理解地试图摆脱它,他们也会以错误告终。

标签: javascript node.js typescript async-await chaining


【解决方案1】:

问题

TypeScript 在做什么?

用于评估 await 的操作数类型的 TypeScript 算法是这样的(这是一个非常简单的解释)[reference]

  1. 类型是承诺吗?如果是,请使用承诺的类型转到步骤 1。如果没有,请转到第 2 步
  2. 类型是 thenable 吗?如果没有返回类型。如果是,则抛出一个类型错误,指出“'await' 操作数的类型必须是有效的承诺或不得包含可调用的 'then' 成员 (ts1320)”。

TypeScript 在您的示例中做了什么?

现在知道了这一点,我们可以在检查代码时看到 TypeScript 在做什么。

await 的操作数是:

new QueryBuilder(Model.find())
.limit(5)
.skip(5)
  1. skip 的调用不会返回承诺。我们进入第 2 步(注意:对limit 的调用或QueryBuilder 的实例化都不会。
  2. skip 返回 QueryBuilder 的实例,它有一个可调用的 then 成员。这会导致类型错误:“'await' 操作数的类型必须是有效的承诺或不得包含可调用的 'then' 成员 (ts1320)”。

带有可调用“then”成员的类定义:

class QueryBuilder {
  public query: Cursor;
  constructor(query: Cursor) {
      this.query = query;
  }
  
  ...
  
  public then(cb: Function) {
      cb(this.query.toArray());
  }
}

为什么 TypeScript 会出错?

现在我们了解了 TypeScript 是如何抛出类型错误的。但是为什么会抛出这个错误呢? JavaScript 让你await任何东西上。

[rv] = await expression;

表达式:一个 Promise 或任何要等待的值。
rv:返回 promise 的已实现值,如果不是 Promise,则返回值本身。

——MDN documentation on await

为什么 TypeScript 会说“'await' 操作数的类型 [如果它不是有效的承诺] 不能包含可调用的 'then' 成员”?为什么它不让你 await 在 thenable 上? MDN 甚至给出了一个例子,你在 thenable 上 await

async function f2() {
  const thenable = {
    then: function(resolve, _reject) {
      resolve('resolved!')
    }
  };
  console.log(await thenable); // resolved!
}

f2();

——MDN example awaiting a thenable

TypeScript 的源代码被注释掉了。上面写着:

该类型不是承诺,因此无法进一步展开。 只要该类型没有可调用的“then”属性,它就是 安全返回类型;否则报错,我们返回 未定义。

一个非承诺“thenable”的例子可能是:

await { then(): void {} }

“thenable”与 promise 的最小定义不匹配。 当 Promise/A+-compatible 或 ES6 Promise 尝试采用此值时, 承诺永远不会兑现。我们将此视为错误以帮助标记 运行时问题的早期指标。如果用户想要返回 这个值来自异步函数,他们需要将它包装在一些 其他值。如果他们希望它被视为一个承诺,他们可以施放 到&lt;any&gt;

——Reference

通过阅读本文,我的理解是 TypeScript 不会 await 在 non-promise thenables 上,因为它不能保证实现符合 Promises/A+ 定义的最低规范,因此假定它是一个错误。

对您的解决方案的评论

在您尝试并添加到问题上下文的解决方案中,您定义了 QueryBuilder 类以扩展原生承诺,然后您覆盖了 then 成员。虽然这似乎具有您想要的效果,但存在一些问题:

你的类实例化有不合理的行为

作为扩展类的结果,您需要先调用其父构造函数,然后才能引用this 上下文。父类的构造函数类型为:

(resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void

正如您发现的那样,您需要传入满足该合同的一些东西,除了让它工作之外别无他法。同样在实例化之后,您的类返回一个用任意值解析的承诺。无关紧要且不合理的代码会导致混乱、意外行为并且可能会出现错误。

你已经破坏了 Promise 接口定义的类型契约

接口定义了catch等其他方法。类的使用者可能会尝试使用catch 方法,虽然合同允许他们这样做,但行为并不如预期。这就引出了下一点。

你没有使用 TypeScript 来发挥它的优势

您在问题中提到:

原版 JavaScript 中的代码运行良好,但是当我尝试用 TypeScript 编写它时,我得到了错误

类型的存在是有原因的,一个是它们通过强制接口之间的契约来减少错误的可能性。如果您尝试绕过它们,则会导致混乱、意外行为和错误风险增加。为什么首先使用 TypeScript?

解决方案

现在我们了解了错误是什么以及发生的原因,我们可以找到解决方案。该解决方案将需要实现 then 成员以满足 Promises/A+ 中的最低规范,因为我们已确定这是导致错误的原因。我们只关心then 接口的规范,而不是它的实现细节:

  • 2.2.1 onFulfilledonRejected 都是可选参数
  • 2.2.7 then 必须返回一个承诺

then 的 TypeScript 定义也可以参考(注意:为了可读性,我做了一些更改):

/**
 * Attaches callbacks for the resolution and/or rejection of the Promise.
 * @param onfulfilled The callback to execute when the Promise is resolved.
 * @param onrejected The callback to execute when the Promise is rejected.
 * @returns A Promise for the completion of which ever callback is executed.
 */
then<
  TResult1 = T,
  TResult2 = never
>(
  onfulfilled?:
    | ((value: T) => TResult1 | PromiseLike<TResult1>)
    | undefined
    | null,
  onrejected?:
    | ((reason: any) => TResult2 | PromiseLike<TResult2>)
    | undefined
    | null
): Promise<TResult1 | TResult2>;

实现本身可能会遵循这个算法:

  1. 执行您的自定义逻辑
  2. 如果第 1 步成功解决了您的自定义逻辑的结果,否则拒绝并出现错误

这是一个示例实现的demo,它可以帮助您开始自己的实现。

class CustomThenable {
  async foo() {
    return await 'something';
  }

  async then(
    onFulfilled?: ((value: string) => any | PromiseLike<string>) | undefined | null,
  ): Promise<string | never> {
    const foo = await this.foo();
    if (onFulfilled) { return await onFulfilled(foo) }
    return foo;
  }
}

async function main() {
  const foo = await new CustomThenable();
  console.log(foo);
  const bar = await new CustomThenable().then((arg) => console.log(arg));
  console.log(bar);
}

main();

【讨论】:

  • 我认为您回答的问题与 OP 提出的问题不同;)在 JavaScript 中,您可以等待“任何值”(例如 await 42)并且当该值具有可调用的 then成员,它将execute that and return its resolved value instead。 OP 的代码适用于 JavaScript,但不适用于 TypeScript。问题是为什么它在 TypeScript 中不起作用。为什么“必须 [它] 不包含可调用的 'then' 成员”?
  • @str:错误的第一部分(“'await' 操作数的类型必须是有效的承诺”)是一个红鲱鱼。我认为 TS 会从字面上强制执行,但一些实验表明它没有。原来这是因为该类有一个可调用的then 成员,这是错误的第二部分。
  • 是的,但是为什么在 TypeScript 中“必须 [它] 不包含可调用的 'then' 成员”,而这在 JavaScript 中是完全有效的(并且并不少见)? OP 希望等待查询执行,而不是查询本身。
  • 这是一个有趣的情况,因为 ES async/await 在 thenables 上运行,而不仅仅是 Promise。
  • 问题是......简而言之,如果我有一个名为 a 的承诺。而且我按顺序调用a.then(..); a.then(...);,所以没有链接..承诺的期望是我得到相同的结果,而不是重新执行操作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-30
  • 2012-04-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多